Winform同步调用异步函数死锁原因分析、为什么要用异步

几年前,一个开发同学遇到同步调用异步函数出现死锁问题,导致UI界面假死。我解释了一堆,关于状态机、线程池、WindowsFormsSynchronizationContext.Post、control.BeginInvoke、APC、IOCP,结果我也没讲明白、他也没听明白。后来路过他座位时看到他在各种摸索、尝试,使用Task、await、async各种组合,当时的场景是这样的:

1

。问题有点复杂,随着那个开发同学离职转做产品后,就不了了之了。工作中许多同事对于同步、异步也不是特别了解,我会以执行流程图表加源码的形式表述,希望通过这篇文章最少能让大家了解.NET的async await出现deadlock的原因,最好能粗略了解async状态机机制、.NET在不同平台网络调用实现机制。如果文章中表述存在问题,欢迎指正。

2、场景再现、执行过程解析 Winform死锁场景

如下代码,如果点击按钮触发btn_realDead_Click事件,Ui线程将挂起在DeadTask().Result陷入死锁。

死锁产生的原因: Ui线程阻塞等待Task完成,Task需要通过Ui线程设置完成结果。

private void btn_realDead_Click(object sender, EventArgs e) { var result = DeadTask().Result; // UI线程挂起位置 PrintInfo(result); } /// <summary> /// /// </summary> /// <returns></returns> private async Task<string> DeadTask() { await Task.Delay(500); return await Task.FromResult("Hello world"); } 场景模拟,解析WindowsFormsSynchronizationContext.Post执行过程

Demo代码地址 : https://gitee.com/RiverBied/async-demo

死锁模拟代码

使用async关键字将会由编译器生成状态机代码,反编译的代码也不太直观,所以我先使用非async代码进行简化模拟,async代码下文在解析。

死锁产生的原因: Ui线程阻塞等待Task完成,Task需要通过Ui线程设置完成结果。

解除死锁: 通过其他线程设置Task完成结果,Ui线程等到Task完成信号继续执行,死锁得到解除。

image-20211016172925842

点击模拟死锁后,输出信息:

image-20211016175145305

执行过程

相信大家看完下面这个图,会有更直观认识。可以看到CurrentSynchronizationContext.Post的SendOrPostCallback内容被包装为ThreadMethodEntry写入到窗体的队列对象的_threadCallbackList。但是 _threadCallbackList什么触发的,采用的是User32 MessageW异步消息接口,最后在UI线程空闲时系统触发窗体回调函数WndProc。

image-20211017114752877

CurrentSynchronizationContext=WindowsFormsSynchronizationContext

WindowsFormsSynchronizationContext设置代码:

// 示例代码 public Form1() { InitializeComponent(); CurrentSynchronizationContext = SynchronizationContext.Current; var controlToSendToField = typeof(WindowsFormsSynchronizationContext).GetField("controlToSendTo", BindingFlags.Instance | BindingFlags.NonPublic); // controlToSendTo设置为当前窗口对象,让重写的WndProc执行接收到消息 controlToSendToField.SetValue(CurrentSynchronizationContext, this); }

WindowsFormsSynchronizationContext.Post源码:

SynchronizationContext.Post功能为发送一个异步委托消息,不阻塞当前线程,委托消息需要在SynchronizationContext绑定线程进行执行。在死锁模拟场景中SynchronizationContext绑定的为Ui线程,所以委托消息需要在Ui线程进行执行。

//源码地址: //https://github.com/dotnet/winforms/blob/release/5.0/src/System.Windows.Forms/src/System/Windows/Forms/WindowsFormsSynchronizationContext.cs#L90 public override void Post(SendOrPostCallback d, object state) { // 调用form1窗口对象的BeginInvoke controlToSendTo?.BeginInvoke(d, new object[] { state }); }

Control.BeginInvoke

BeginInvoke关键源码:

// 定义保证在整个系统中唯一的窗口消息,消息值可用于发送或发布消息,返回窗口消息标识(int)。 s_threadCallbackMessage = User32.RegisterWindowMessageW(Application.WindowMessagesVersion + "_ThreadCallbackMessage"); // 将回调函数执行信息添加到回调函数队列,回调函数即为WindowsFormsSynchronizationContext.Post的SendOrPostCallback参数,_threadCallbackList为Control字段 _threadCallbackList.Enqueue(tme); // 在与创建指定窗口的线程关联的消息队列中放置(发布)一条消息,并在不等待线程处理消息的情况下返回 User32.PostMessageW(this, s_threadCallbackMessage);

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zzsggs.html