当前位置:首页 > C#编程 > C#/.net框架 > 正文内容

C#异步async/await在WinForm中的使用

Jorge2年前 (2022-05-09)C#/.net框架889

 

WinForm窗体中应用异步

WinForm虽然比较老,但是现在还有很多的实际生产项目再用,而且微软在新的.Net core 框架中重新重构了WinForm和WPF,就证明WinForm还是有很大的市场的,微软并没有放弃这项技术,并且将它开源了出来,推陈出新,意义可想而知。

以前的WinForm项目大多数是用基础的多线程技术来实现的,或者用线程池将事件扔到并发队列中去异步执行,很少有用async/await异步方式来实现的,正巧我最近在学习这方面的知识,也浏览了几位大神的博客,在此总结汇总一下。这次我主要是通过一个非常简单的后台累加求和计算赋值的例子来总结异步async/await的各种用法。如果有不妥或待完善之处,欢迎大家给予积极的指正,共同学习,共同进步。


1、旧式的跨线程调用

WinForm这种UI类的编程,即时响应是十分重要的,否则会影响用户的体验,后台的逻辑执行不能影响前端的用户操作,不能卡顿,不能出现线程安全问题,在老式的跨UI线程调用的时候,一般采取异步另起一个后台线程的方式来进行。

C#
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事件中调用异步方法,因为这个事件是同步的,所以我们就需要令外包一层异步方法。

C#
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定义截图

image.png

  • 按钮异步点击事件范例代码。

C#
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的原话讲就是:

C#
默认情况下,当等待未完成的 Task 时,会捕获当前“上下文”,在 Task 完成时使用该上下文恢复方法的执行。
 此“上下文”是当前 SynchronizationContext(除非它是 null,这种情况下则为当前 TaskScheduler)。 
 GUI 和 ASP.NET 应用程序具有 SynchronizationContext,它每次仅允许一个代码区块运行。 
 当 await 完成时,它会尝试在捕获的上下文中执行 async 方法的剩余部分。 
 但是该上下文已含有一个线程,该线程在(同步)等待 async 方法完成。 它们相互等待对方,从而导致死锁。

为此大神给出了解决办法,第一种就是自始至终一直使用异步方法,第二种是使用 ConfigureAwait(false)。
例如:await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);

C#
通过使用 ConfigureAwait,可以实现少量并行性: 某些异步代码可以与 GUI 线程并行运行,而不是不断塞入零碎的工作。
除了性能之外,ConfigureAwait 还具有另一个重要方面: 它可以避免死锁。 
如果向 DelayAsync 中的代码行添加“ConfigureAwait(false)”,则可避免死锁。 
此时,当等待完成时,它会尝试在线程池上下文中执行 async 方法的剩余部分。 
该方法能够完成,并完成其返回任务,因此不存在死锁。 
如果需要逐渐将应用程序从同步转换为异步,则此方法会特别有用。
如果可以在方法中的某处使用 ConfigureAwait,则建议对该方法中此后的每个 await 都使用它。 
前面曾提到,如果等待未完成的 Task,则会捕获上下文;如果 Task 已完成,则不会捕获上下文。 
在不同硬件和网络情况下,某些任务的完成速度可能比预期速度更快,需要谨慎处理在等待之前完成的返回任务

但也有一种情况,不能使用ConfigureAwait(false)来放弃UI的上下文:

C#
如果方法中在 await 之后具有需要上下文的代码,则不应使用 ConfigureAwait。 
对于 GUI 应用程序,包括任何操作 GUI 元素、编写数据绑定属性或取决于特定于 GUI 的类型(如 Dispatcher/CoreDispatcher)的代码。 
演示 GUI 应用程序中的一个常见模式:让 async 事件处理程序在方法开始时禁用其控制,执行某些 await,然后在处理程序结束时重新启用其控制;
因为这一点,事件处理程序不能放弃其上下文。
C#
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 方法中,仅在上下文相关事件处理程序中保留最少量的代码。

示例代码:

C#
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;
  }}




————————————————

版权声明:本文为CSDN博主「卷儿哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/DahlinSky/article/details/104555975

扫描二维码推送至手机访问。

版权声明:本文由7点博客发布,如需转载请注明出处。

本文链接:http://6dot.cn/?id=90

标签: .NET.NET框架
分享给朋友:

“C#异步async/await在WinForm中的使用” 的相关文章

C# 控件闪烁问题的解决

C# 控件闪烁问题的解决

说一下解决C#下控件闪烁的几个问题,如下:  listview和datagridview显示数据闪烁 自定义控件的显示闪烁listbox滚动条拖动闪烁面板中控件过多的闪烁propertyGrid点击和修改项目缓慢的问题richtextbox控件的刷新显示问题此类问题对于界面复杂规...

C#字符串与享元(Flyweight)模式

C#字符串与享元(Flyweight)模式

注:关注这个话题是因为看到C#的关键字 lock时,其传入引用对象。因为string也是引用对象,所以能否做为lock的参数?对于这个问题,要搞明白C#的字符串的一个特点,它使用类似于享元模式的机制。因此在lock中锁字符串是相当不安全的。下面贴子是对C#字符串与享元模式的深入讨论。写这个文章,主要...

C# Modelbus crc16计算校验和程序

C# Modelbus crc16计算校验和程序

我们手里一个无刷电机,采用485的modelbus crc16协议来控制。因此需要一个计算校验和的工具。源码:C#using System;using System.Collections.Generic;using System.ComponentModel;usin...

C# tableLayoutPanel动态加载控件闪烁的解决方案

C# tableLayoutPanel动态加载控件闪烁的解决方案

WinForm加载多个自定义控件时,会出现很严重的闪烁问题,很卡,一块一块的加载(像打开网页时,网络很卡的那种感觉)简直没法忍受。在网上搜索了好久,网上大部分的方法是一下4种,但是都不能有效的解决问题。  1.将DoubleBuffered 设置 true,用双缓存处理Form界面内容加载,可以提高...

谈谈ObservableCollection观察者集合

谈谈ObservableCollection观察者集合

注:事件很常见,但有重复的代码量。如果你想简化一下事件的编程,而且是整个程序只使用一个像事件池一样的东西,可以尝试使用下文讲的ObservableCollection观察者集合来做到。ObservableCollection<T>类表示一个动态数据集合,在添加项,移除项或刷新整个列表时,...

.NET(C#) TPL:Task中未觉察异常和TaskScheduler.UnobservedTaskException事件

.NET(C#) TPL:Task中未觉察异常和TaskScheduler.UnobservedTaskException事件

当你在一个Task执行中抛出异常,比如:C#Task.Factory.StartNew(() =>{    throw new Exception();});运行该方法,没有任何异常抛出。事实上此时Task的异常处于未觉察状...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。