C#异步asyncawait在WinForm中的使⽤
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;
};
for(int i =0; i <10000; 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(()=>
await和async使用方法{
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⼤神的⼀篇博⽂ 后,我了解到,有时候将整个项⽬改造成异步实现,会陷⼊很多的坑中,尤其是异步和同步混合使⽤时,弄不好就会阻塞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)”,则可避免死锁。 此时,当等待完成时,它会尝试在线程池上下⽂中执⾏ a
sync ⽅法的剩余部分。
该⽅法能够完成,并完成其返回任务,因此不存在死锁。 如果需要逐渐将应⽤程序从同步转换为异步,则此⽅法会特别有⽤。
如果可以在⽅法中的某处使⽤ 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;
}
}
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论