什么情况下会产生死锁?
调用 Task.Wait()
或者 Task.Result
立刻产生死锁的充分条件:
- 调用 Wait() 或 Result 的代码位于 UI 线程;
- Task 的实际执行在其他线程,且需要返回 UI 线程。
死锁的原因:
UWP、WPF、Windows Forms 程序的 UI 线程都是单线程的。为了让使用了 async/await 的代码像使用同步代码一样简单,UWP、WPF、Windows Forms 程序的 Application 类在构造的时候会将主 UI 线程 Task 的同步上下文设置为 DispatcherSynchronizationContext
的实例.当 Task 的任务结束时,会从 AsyncMethodStateMachine
中调用 Awaiter
的 OnComplete()
方法,而 await 后续方法的执行靠的就是 OnComplete()
方法中一层层调用到 DispatcherSynchronizationContext
里的 Post 方法:
/// <summary>
/// Asynchronously invoke the callback in the SynchronizationContext.
/// </summary>
public override void Post(SendOrPostCallback d, Object state)
{
// Call BeginInvoke with the cached priority. Note that BeginInvoke
// preserves the behavior of passing exceptions to
// Dispatcher.UnhandledException unlike InvokeAsync. This is
// desireable because there is no way to await the call to Post, so
// exceptions are hard to observe.
_dispatcher.BeginInvoke(_priority, d, state);
}
如果 _dispatcher.BeginInvoke(_priority, d, state);
这句代码在后台线程,那么此时 UI 线程处于 Wait()/Result 调用中的阻塞状态,BeginInvoke 中的任务是无论如何也无法执行到的!于是无论如何都无法完成这个 Post 任务,即无论如何也无法退出此异步任务的执行,于是 Wait() 便无法完成等待……死锁……无论是 WPF 还是 UWP,只要在 UI 线程上调用上述代码,必然死锁!
WinForm 同步上下文
位于:System.Windows.Forms.dll
: System.Windows.Forms
- WinForm应用程序会创建并安装一个
WindowsFormsSynchronizationContext
作为创建 UI Control 的每个线程的当前上下文
一个 WinForm 应用程序对应一个同步上下文
- 这一
SynchronizationContext
使用 UI Control 的Invoke
等方法(ISynchronizeInvoke
派生出来的),该方法将委托传递给基础 Win32 消息循环 WindowsFormsSynchronizationContext
的上下文是一个单例的 UI 线程- 在
WindowsFormsSynchronizationContext
列队的所有委托一次一个地执行
这个已排序的委托队列,被一个特定 UI 线程执行完
什么情况下不会产生死锁?
读写文件,访问网络,这些 IO 阻塞的操作执行时,里面根本就没有线程,还有另一些操作,也没有后台线程的参与,于是也不存在从后台线程回到主线程导致死锁的情况。如 Task.Yield,还有 InvokeAsync,它们也不会造成死锁。
另外,如果是控制台程序,或者一个普通的非 UI 线程,其 SynchronizationContext 为 null,那么异步任务执行完后不需要回到原有线程,也不会造成死锁。
总结不会造成死锁的充分条件:
- 异步操作执行完后不需要回到原有线程(例如非 UI 线程和控制台线程);
- 异步操作不需要单独的线程执行任务。
如何避免死锁?
明确了会造成死锁的条件和不会造成死锁的条件后,我们只需要做到以下几点即可避免死锁了:
- 在 UI 线程,如果使用了
async/await
,就尽量不要再使用Task.Wait()/Task.Result
了,就一直异步一条路走到黑好了(微软称其为 Async All the Way)。微软将其描述为:async/await 会像病毒一样在你的代码中传播。 - 如果可能,尽量在异步任务后添加 .ConfigureAwait(false);这样,异步任务后面继续执行的代码就不会回到原UI线程了,而是直接从线程池中再取出一个线程执行;这样,即便 UI 线程后续可能有别的原因造成阻塞,也不会产生死锁了。
UI线程同步解决方案:
Task包装一层
public virtual IList<Model> SelectDatas()
{
return Task.Run(() =>
{
var service = IocRuntime.Instance.GetService<IModelService>();
var tsk = service.QueryAllAsync(x => x.a == "a");
return tsk;
}).Result;
}