C#异步async/await在WinForm中的使用
以前的WinForm项目大多数是用基础的多线程技术来实现的,或者用线程池将事件扔到并发队列中去异步执行,很少有用async/await异步方式来实现的,正巧我最近在学习这方面的知识,也浏览了几位大神的博客,在此总结汇总一下。
WinForm窗体中应用异步
WinForm虽然比较老,但是现在还有很多的实际生产项目再用,而且微软在新的.Net core 框架中重新重构了WinForm和WPF,就证明WinForm还是有很大的市场的,微软并没有放弃这项技术,并且将它开源了出来,推陈出新,意义可想而知。
以前的WinForm项目大多数是用基础的多线程技术来实现的,或者用线程池将事件扔到并发队列中去异步执行,很少有用async/await异步方式来实现的,正巧我最近在学习这方面的知识,也浏览了几位大神的博客,在此总结汇总一下。这次我主要是通过一个非常简单的后台累加求和计算赋值的例子来总结异步async/await的各种用法。如果有不妥或待完善之处,欢迎大家给予积极的指正,共同学习,共同进步。
1、旧式的跨线程调用
WinForm这种UI类的编程,即时响应是十分重要的,否则会影响用户的体验,后台的逻辑执行不能影响前端的用户操作,不能卡顿,不能出现线程安全问题,在老式的跨UI线程调用的时候,一般采取异步另起一个后台线程的方式来进行。
private void button4_Click(object sender, EventArgs e)
{
Debug.WriteLine("button4 Thread ID :" + Thread.CurrentThread.ManagedThreadId);
Thread thread1 = new Thread(SetValues);
thread1.IsBackground = true;
thread1.Start();
}
private void SetValues()
{
Debug.WriteLine("button4 Thread ID :" + Thread.CurrentThread.ManagedThreadId);
int sum = 0;
Action<int> setVal = (i) => {
sum += i;
this.textBox4.Text = sum.ToString();
};
for (int i = 0; i < 10000; i++)
{
this.textBox4.Invoke(setVal, i);
}
}
通过打印当前线程ID,我们发现确实是不同的两个线程。
Current Thread ID :1
Current Thread ID :3
2、同步事件中调用异步方法
我们在同步的button4_Click事件中调用异步方法,因为这个事件是同步的,所以我们就需要令外包一层异步方法。
private void button3_Click(object sender, EventArgs e)
{
Debug.WriteLine("button3 Thread ID :" + Thread.CurrentThread.ManagedThreadId);
ToDo();
}
private async void ToDo()
{
Debug.WriteLine("button3 Thread ID :" + Thread.CurrentThread.ManagedThreadId);
textBox3.Text = await DoSomeWork();
}
private Task<string> DoSomeWork()
{
int sum = 0;
return Task.Run(() =>
{
Debug.WriteLine("button3 Thread ID :" + Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 10000; i++)
{
sum += i;
}
return sum.ToString();
});
}
3. 异步事件中调用异步方法
我没有仔细研究过WinForm的异步事件机制,但是很神奇的是它居然天然的支持控件的async异步事件,后来我F12了一下发现了是因为Control控件基类继承了ISynchronizeInvoke接口,而该接口要求实现返回IAsyncResult 类型的BeginInvoke(Delegate method, object[] args);的方法,我猜应该是这样的。
- WinForm控件事件F12定义截图
- 按钮异步点击事件范例代码。
private async void button1_ClickAsync(object sender, EventArgs e)
{
var t1 = Task.Run(() =>
{
System.Diagnostics.Debug.WriteLine("button1 Thread ID :" + Thread.CurrentThread.ManagedThreadId);
return "开始计算..";
});
var t3 = Task.Run(() =>
{
Debug.WriteLine("button1 Thread ID :" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(3000);
return " 计算完成! ";
});
int sum = 0;
Debug.WriteLine("button1 Thread ID :" + Thread.CurrentThread.ManagedThreadId);
var t2= Task.Run(() =>
{
System.Diagnostics.Debug.WriteLine("button1 Thread ID :" + Thread.CurrentThread.ManagedThreadId);
for (int i = 0; i < 10000; i++)
{
sum += i;
}
return sum.ToString();
});
textBox1.Text += await t1;
await Task.Delay(TimeSpan.FromSeconds(1));
textBox1.Text += await t2;
textBox1.Text += await t3;
}
4. 异步线程阻塞和死锁警告
通过读Stephen Cleary大神的一篇博文 Async/Await
异步编程中的最佳做法后,我了解到,有时候将整个项目改造成异步实现,会陷入很多的坑中,尤其是异步和同步混合使用时,弄不好就会阻塞UI线程和死锁,用Stephen Cleary的原话讲就是:
默认情况下,当等待未完成的 Task 时,会捕获当前“上下文”,在 Task 完成时使用该上下文恢复方法的执行。 此“上下文”是当前 SynchronizationContext(除非它是 null,这种情况下则为当前 TaskScheduler)。 GUI 和 ASP.NET 应用程序具有 SynchronizationContext,它每次仅允许一个代码区块运行。 当 await 完成时,它会尝试在捕获的上下文中执行 async 方法的剩余部分。 但是该上下文已含有一个线程,该线程在(同步)等待 async 方法完成。 它们相互等待对方,从而导致死锁。
为此大神给出了解决办法,第一种就是自始至终一直使用异步方法,第二种是使用 ConfigureAwait(false)。
例如:await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
通过使用 ConfigureAwait,可以实现少量并行性: 某些异步代码可以与 GUI 线程并行运行,而不是不断塞入零碎的工作。
除了性能之外,ConfigureAwait 还具有另一个重要方面: 它可以避免死锁。 如果向 DelayAsync 中的代码行添加“ConfigureAwait(false)”,则可避免死锁。 此时,当等待完成时,它会尝试在线程池上下文中执行 async 方法的剩余部分。 该方法能够完成,并完成其返回任务,因此不存在死锁。 如果需要逐渐将应用程序从同步转换为异步,则此方法会特别有用。
如果可以在方法中的某处使用 ConfigureAwait,则建议对该方法中此后的每个 await 都使用它。 前面曾提到,如果等待未完成的 Task,则会捕获上下文;如果 Task 已完成,则不会捕获上下文。 在不同硬件和网络情况下,某些任务的完成速度可能比预期速度更快,需要谨慎处理在等待之前完成的返回任务
但也有一种情况,不能使用ConfigureAwait(false)来放弃UI的上下文:
如果方法中在 await 之后具有需要上下文的代码,则不应使用 ConfigureAwait。 对于 GUI 应用程序,包括任何操作 GUI 元素、编写数据绑定属性或取决于特定于 GUI 的类型(如 Dispatcher/CoreDispatcher)的代码。 演示 GUI 应用程序中的一个常见模式:让 async 事件处理程序在方法开始时禁用其控制,执行某些 await,然后在处理程序结束时重新启用其控制;因为这一点,事件处理程序不能放弃其上下文。
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// Can't use ConfigureAwait here ...
await Task.Delay(1000);
}
finally
{
// Because we need the context here.
button1.Enabled = true;
}
}
而且大神给出了解决办法,他建议将事件处理程序的所有核心逻辑都置于一个可测试且无上下文的 async Task 方法中,仅在上下文相关事件处理程序中保留最少量的代码。事例代码:
private async Task HandleClickAsync()
{
// Can use ConfigureAwait here.
await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
}
private async void button1_Click(object sender, EventArgs e)
{
button1.Enabled = false;
try
{
// Can't use ConfigureAwait here.
await HandleClickAsync();
}
finally
{
// We are back on the original context for this method.
button1.Enabled = true;
}
}
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)