使用TaskCompletionSource和AutoResetEvent同步时出现奇怪死锁的原因
异步代码中AutoResetEvent等待超时的原因分析
运行以下C#程序时,会随机出现两种不同的执行结果:结果1频繁出现等待超时(并非真正死锁,最终仍能完成),结果2则正常执行。以下是对该现象的具体解释。
程序代码
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"); } } }
执行结果
结果1(频繁出现)
<<<<<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
结果2
<<<<<Result2 >>>>>> First Wait OK
原因分析
核心原因:异步延续的调度时序与阻塞调用的冲突
这不是真正的死锁,而是线程调度的时序差异导致的等待超时,关键在于func方法中await Task.Delay(10)完成后,后续代码的执行时机和主线程的阻塞操作之间的交互:
- 调用
func.Invoke()时,方法执行到await Task.Delay(10)会立即返回未完成的Task,主线程继续执行await tcs.Task。 Task.Delay(10)完成后,func的剩余代码(tcs.SetResult(true); evtFinished.Set();)会被调度到线程池线程执行(控制台应用无同步上下文,await默认回退到线程池)。
结果2(正常执行)的场景
func的后续代码中,tcs.SetResult(true)和evtFinished.Set()几乎同时执行,主线程在await tcs.Task恢复执行时,evtFinished已经被设置为有信号状态,因此evtFinished.WaitOne(1000)能立即获取信号,输出"First Wait OK"。
结果1(等待超时)的场景
func中tcs.SetResult(true)执行后,主线程的await tcs.Task立即恢复执行,随即调用evtFinished.WaitOne(1000)阻塞当前线程。但此时evtFinished.Set()还未执行——由于线程调度的延迟,func的剩余代码可能还在等待线程池线程的调度,导致evtFinished仍处于无信号状态,主线程的等待因此超时。
当主线程执行到await t;时,会异步等待func的Task完成,此时func的剩余代码(evtFinished.Set())会被执行完毕,之后evtFinished.WaitOne()就能获取信号,最终输出"Wait OK"。
关键总结
AutoResetEvent.WaitOne本身不会引发死锁,但在异步代码中混合使用阻塞调用(如WaitOne)和异步等待时,线程调度的时序差异会导致这类等待超时的现象。- 避免此类问题的最佳实践是:异步代码中尽量使用异步等待(如
Task相关的API),而非阻塞式的同步原语调用。
内容的提问来源于stack exchange,提问作者Sky Nano
相关产品推荐
相关产品推荐

