提问人:Sky Nano 提问时间:11/10/2023 最后编辑:Theodor ZouliasSky Nano 更新时间:11/10/2023 访问量:67
与 TaskCompletionSource 和 AutoResetEvent 同步时出现奇怪的死锁
Strange deadlock while sync with TaskCompletionSource and AutoResetEvent
问:
在运行以下 C# 程序时,我随机得到了两个不同的结果。 结果 1(经常发生)实际上是死锁的。请向我解释为什么会这样。 我不希望在AutoResetEvent上使用WaitOne时出现死锁。
class Program
{
static async Task Main()
{
TaskCompletionSource<bool> tcs = new();
AutoResetEvent evtFinished = new AutoResetEvent(false);
Func<Task> func = async Task () =>
{ await Task.Delay(10); tcs.SetResult(true); evtFinished.Set(); };
var t = func.Invoke();
//await func.Invoke();
await tcs.Task;
if (evtFinished.WaitOne(1000))
Console.WriteLine("First Wait OK");
else
{
Console.WriteLine("First Wait Timeout");
for (int i = 0; i < 10 && !evtFinished.WaitOne(100); i++)
Console.WriteLine($" {i+1} Wait Timeout");
await t;
evtFinished.WaitOne();
Console.WriteLine("Wait OK");
}
}
}
<<<<<Result 1: >>>>>>
# First Wait Timeout
# 1 Wait Timeout
# 2 Wait Timeout
# 3 Wait Timeout
# 4 Wait Timeout
# 5 Wait Timeout
# 6 Wait Timeout
# 7 Wait Timeout
# 8 Wait Timeout
# 9 Wait Timeout
# 10 Wait Timeout
# Wait OK
<<<<<Result2 >>>>>
# First Wait OK
答:
1赞
Yarik
11/10/2023
#1
默认情况下,同步运行其延续(在大多数情况下)。TaskCompletionSource.SetResult
Task
这意味着在您的情况下,之后和直到下一个的所有内容都将在到达之前运行。await tcs.Task
await
evtFinished.Set()
有多种方法可以解决这个问题,但最简单的方法是将标志传递给构造函数。TaskContinuationOptions.RunContinuationsAsynchronously
TaskCompletionSource
评论
0赞
Sky Nano
11/10/2023
谢谢。解决方案有效。
0赞
user16606026
11/10/2023
如果解决方案有效,请将答案标记为已接受~
1赞
Stephen Cleary
11/10/2023
#2
发生这种情况有几个综合原因。首先,await
的基本行为(如我的博客所述)是它捕获一个“上下文”,并且 - 默认情况下 - 在该“上下文”中恢复。在这种情况下,没有上下文,因此方法在线程池线程上恢复。其次,当 awaitable 完成时,如果完成 awaitable 的线程与 捕获的上下文兼容,它将同步执行该延续。更具体地说,使用 .async
await
await
TaskContinuationOptions.ExecuteSynchronously
在我的博客文章 不要阻塞异步代码 中更深入地探讨了此方案。
因此,通过它:
Main
在主线程上启动并调用 .func.Invoke
func.Invoke
命中它并返回一个未完成的任务,存储在 中。await
t
Main
继续执行(仍在主线程上)并执行 .await tcs.Task
- 假设 尚未完成(这是争用条件),则返回未完成的任务。
tcs
Main
- 完成后,线程池线程将用于恢复执行。
Task.Delay
func
- 此线程池线程调用 ,它现在附加了一个延续(其余部分)。
tcs.SetResult
Main
- 由于线程池线程与捕获的上下文(它是线程池线程)兼容,因此它只是同步执行延续(其余部分)。
Main
- 然后,此线程会同步阻止等待设置,但不会。请注意,此时您的调用堆栈是反转的:实际上调用了延续。您可以通过使延迟更大并在调试器中运行来验证这一点。
evtFinished
func
Main
- 最终,同一个线程池线程将命中,它最终返回给其调用方,允许完成运行,设置 .
await t
func
evtFinished
评论
0赞
Sky Nano
11/10/2023
非常感谢您的回答。解释得很好。
1赞
JonasH
11/10/2023
在顶部,您提到“没有上下文”,但在步骤 7 中,您说“线程池上下文”。我认为这可以说得更清楚一点。
0赞
Guru Stron
11/10/2023
@SkyNano答案是否适合您 - 不要忘记将其标记为已接受。
0赞
Theodor Zoulias
11/10/2023
后面的线程是完成 awaitable 的线程。这可以很容易地通过实验来证明。缺省同步上下文是 ,而不是 。await
null
ThreadPool
1赞
Stephen Cleary
11/11/2023
我会说有一个兼容性检查,但你是对的,当没有捕获上下文时,检查总是通过。
评论