「C#」异步编程玩法笔记-WinForm中的常见问题

news/2024/11/13 21:30:41/

目录

1、异步更新界面

1.1、问题

1.2、解决问题

1.3、AsyncOperationManager和AsyncOperation

1.4、Invoke、BeginInvoke、EndInvoke及InvokeRequired

Invoke

InvokeRequired

BeginInvoke

EndInvoke

2、死锁

2.1、问题

2.2、 解决方法

2.2.1、不要await

2.2.2、用await代替Wait()/Result

2.2.3、使用新的异步方法中转

2.2.4、ConfigAwaiter(false)

3、ConfigAwaiter(false)


1、异步更新界面

1.1、问题

先新建个简单winform窗体程序(取名WinFormsTPL)

界面及按钮实现如下:

namespace WinFormsTPL
{public partial class Form1 : Form{public Form1(){InitializeComponent();}private void btnAsyncUpdate_Click(object sender, EventArgs e){Task.Factory.StartNew(() =>{    this.lbText.Text = "你好,世界!";});}}
}

然后运行,就能得到WinForm开发中做异步编程时最常遇到的问题了,就是下面这个报错。

简单的理解就是不能跨线程访问UI。因为UI的变更绘制有专门的线程。

但是深究这个问题,法相想理解清楚似乎有点难度。看了很多资料,总是逃不过两个主要的动东西:UI线程和同步上下文(SynchronizationContext)。

具象化一点,打个可能不恰当的比喻,公司里面办事的员工相当于线程,部门以及办公室相当于同步上下文。员工(线程)的工作需要办公场所(同步上下文)。但员工可以在不同办公场所穿行走动去完成他的工作,例如去装配间组转设备然后去厂房调试设备,然后去办公室写ppt……

看一下巨硬家大佬的文章怎么说的(似乎有点久远):

MSDN 杂志:并行计算 - SynchronizationContext 综述 | Microsoft Learn

SynchronizationContext 的实际“上下文”并没有明确的定义。不同的框架和主机可以自行定义自己的上下文。通过了解这些不同的实现及其限制,可以清楚了解 SynchronizationContext 概念可以和不可以实现的功能。我将简单讨论部分实现。

WindowsFormsSynchronizationContext(System.Windows.Forms.dll:System.Windows.Forms)Windows 窗体应用程序会创建并安装一个 WindowsFormsSynchronizationContext 作为创建 UI 控件的任意线程的当前上下文。这一 SynchronizationContext 使用 UI 控件的 ISynchronizeInvoke 方法,该方法将委托传递给基础 Win32 消息循环。WindowsFormsSynchronizationContext 的上下文是一个单独的 UI 线程。

在 WindowsFormsSynchronizationContext 列队的所有委托一次一个地执行;它们通过一个特定 UI 线程执行以便列队。当前实现为每个 UI 线程创建一个 WindowsFormsSynchronizationContext。

DispatcherSynchronizationContext(WindowsBase.dll:System.Windows.Threading)WPF 和 Silverlight 应用程序使用 DispatcherSynchronizationContext,这样,委托按“常规”优先级在 UI 线程的调度程序中列队。当一个线程通过调用 Dispatcher.Run 开始其调度程序时,这一 SynchronizationContext 作为当前上下文安装。DispatcherSynchronizationContext 的上下文是一个单独的 UI 线程。

在 DispatcherSynchronizationContext 列队的所有委托一次一个地执行;它们通过一个特定 UI 线程执行以便列队。当前实现为每个顶层窗口创建一个 DispatcherSynchronizationContext,即使它们都使用相同的基础调度程序也是如此。

本人WPF不熟,单说Winform的SynchronizationContext也就是WindowsFormsSynchronizationContext

,作为创建 UI 控件的任意线程的当前上下文。那就是说一个窗体程序(winform)只能有一个同步上下文。

那么能不能在一个同步上下文里启动另一个winfom程序呢?

在窗体上bia一个按钮,按钮事件中调用

Application.Run(new Form2());

ok,报错:

窗体程序的UI线程底层就是消息循环机制,一个线程上只能有一个消息循环。(没有找到比较明确的官方文档说明)

那么对于一个winform程序,其UI线程是单一线程,与其对应的同步上下文(SynchronizationContext)也只有一个。

不过也不是不能有多UI线程的窗体程序,比如这样写就不会报错:

private void btn_Click(object sender, EventArgs e)
{var thread = new Thread(() =>{Form f = new Form();Application.Run(f);});thread.SetApartmentState(ApartmentState.STA);thread.Start();
}

这样即在新线程里启动新窗体,但新的窗体也会有新的同步上下文。

在之前提到的官方文档MSDN 杂志:并行计算 - SynchronizationContext 综述 | Microsoft Learn 中也能看到说明

SynchronizationContext 实例和线程之间没有 1:1 的对应关系。WindowsFormsSynchronizationContext 确实 1:1 映射到一个线程(只要不调用 SynchronizationContext.CreateCopy),但任何其他实现都不是这样。一般而言,最好不要假设任何上下文实例将在任何指定线程上运行。

回过头来再看一下最初的报错信息:“从不是创建控件“xxx”的线程访问它”。即一个控件、一个窗体的同步上下文是在new它的时候确定的,如果在新线程中,则也会有新的同步上下文。

1.2、解决问题

大致了解清楚原有后,解决这个问题的方式就明确了,无非就是两条路,一是回到创建它的线程,二是回到它的同步上下文。

先看第一种

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{Task.Factory.StartNew(() =>{});this.lbText.Text = "你好,世界!";
}

emmm……“避免bug的最好方式就是不写代码!”避免异步报错的方式就是不要异步!

看过废话文学后看第二种方法:

private async void btnAsyncUpdate_Click(object sender, EventArgs e)
{SynchronizationContext currentContext = SynchronizationContext.Current;await Task.Factory.StartNew((c) =>{SendOrPostCallback sendCallback = (o) =>{this.lbText.Text = "你好,世界!";};if (c is WindowsFormsSynchronizationContext context){context.Send(sendCallback, null);}}, currentContext);           
}

即使用SynchronizationContext.Send()方法。将界面操作封送会原有的同步上下文,执行时对控件的赋值自然在原有的同步上下文对应的线程上执行了,就不会报错。

SynchronizationContext有Send和Post()两个常用方法,有很多文章来详细介绍两者不同,总结的说,Send()是封送到同步执行,Post()是异步执行。具体看下源码,结合之前对线程和线程池的说明就很好理解了:

public virtual void Send(SendOrPostCallback d, Object state)
{d(state);
}public virtual void Post(SendOrPostCallback d, Object state)
{ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}

1.3、AsyncOperationManager和AsyncOperation

在VS中编写1.2中方法二的代码时,可以看到VS的一个提示:

即是说SynchronizationContext.Current是可能未空的,实际上控制台程序中该项即默认为空的。

更加推荐使用AsyncOperationManager和AsyncOperation

.NET Framework 中的 AsyncOperationManager 和 AsyncOperation 类是 SynchronizationContext 抽象的轻型包装。AsyncOperationManager 在第一次创建 AsyncOperation 时捕获当前 SynchronizationContext,如果当前 SynchronizationContext 为 null,则使用默认 SynchronizationContext。AsyncOperation 将委托异步发布到捕获的 SynchronizationContext。

最新的.Net7中也是有这两个类的。

public static class AsyncOperationManager
{public static AsyncOperation CreateOperation(object userSuppliedState){return AsyncOperation.CreateOperation(userSuppliedState, SynchronizationContext);}/// <include file='doc\AsyncOperationManager.uex' path='docs/doc[@for="AsyncOperationManager.SynchronizationContext"]/*' />[EditorBrowsable(EditorBrowsableState.Advanced)]public static SynchronizationContext SynchronizationContext{get{if (SynchronizationContext.Current == null){SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());}return SynchronizationContext.Current;}#if SILVERLIGHT// a thread should set this to null  when it is done, else the context will never be disposed/GC'd[SecurityCritical][FriendAccessAllowed]internal set {SynchronizationContext.SetSynchronizationContext(value);}
#else// a thread should set this to null  when it is done, else the context will never be disposed/GC'd[PermissionSetAttribute(SecurityAction.LinkDemand, Name = "FullTrust")]set{SynchronizationContext.SetSynchronizationContext(value);}
#endif}
}

即使用AsyncOperationManager.CreateOperation()实例化AsyncOperation对象时是会判断有没有SynchronizationContext,没有则会创建一个SynchronizationContext,以确保其不为空。

再来看AsyncOperation,源码如下:

namespace System.ComponentModel
{using System.Security.Permissions;using System.Threading;[HostProtection(SharedState = true)]public sealed class AsyncOperation{private SynchronizationContext syncContext;private object userSuppliedState; private bool alreadyCompleted;/// <summary>///     Constructor. Protected to avoid unwitting usage - AsyncOperation objects///     are typically created by AsyncOperationManager calling CreateOperation./// </summary>private AsyncOperation(object userSuppliedState, SynchronizationContext syncContext){this.userSuppliedState = userSuppliedState;this.syncContext = syncContext;this.alreadyCompleted = false;this.syncContext.OperationStarted();}/// <summary>///     Destructor. Guarantees that sync context will always get notified of completion./// </summary>~AsyncOperation(){if (!alreadyCompleted && syncContext != null){syncContext.OperationCompleted();}}public object UserSuppliedState{get { return userSuppliedState; }}/// <include file='doc\AsyncOperation.uex' path='docs/doc[@for="AsyncOperation.SynchronizationContext"]/*' />public SynchronizationContext SynchronizationContext{get{return syncContext;}}public void Post(SendOrPostCallback d, object arg){VerifyNotCompleted();VerifyDelegateNotNull(d);syncContext.Post(d, arg);}public void PostOperationCompleted(SendOrPostCallback d, object arg){Post(d, arg);OperationCompletedCore();}public void OperationCompleted(){VerifyNotCompleted();OperationCompletedCore();}private void OperationCompletedCore(){try{syncContext.OperationCompleted();}finally{alreadyCompleted = true;GC.SuppressFinalize(this);}}private void VerifyNotCompleted(){if (alreadyCompleted){throw new InvalidOperationException(SR.GetString(SR.Async_OperationAlreadyCompleted));}}private void VerifyDelegateNotNull(SendOrPostCallback d){if (d == null){throw new ArgumentNullException(SR.GetString(SR.Async_NullDelegate), "d");}}/// <summary>///     Only for use by AsyncOperationManager to create new AsyncOperation objects/// </summary>internal static AsyncOperation CreateOperation(object userSuppliedState, SynchronizationContext syncContext){AsyncOperation newOp = new AsyncOperation(userSuppliedState, syncContext); return newOp;}}
}

从源码看,一方面可以获取到不为空的SynchronizationContext,并且可以直接使用Post()方式进行调用,Post()内部处理前做了校验,一个委托只能在OperationCompleted()之前调用,使用PostOperationCompleted()即调用一次边关闭,Completed之后内部会调用GC回收这个AsyncOperation对象。

还有一点要说,就是更多的是在基于事件的异步编程中使用的,基于事件的异步编程已经不被推荐,更多的使用基于任务的异步编程。

新组件不应使用基于事件的异步模式。Visual Studio 异步社区技术预览 (CTP) 包含一篇描述基于任务的异步模式的文档,在这种模式下,组件返回 Task 和 Task<TResult> 对象,而不是通过 SynchronizationContext 引发事件。基于任务的 API 是 .NET 中异步编程的发展方向。

1.4、Invoke、BeginInvoke、EndInvoke及InvokeRequired

如1.1和1.2中所说,如果有窗体或者控件(假设Form2)是在新的线程中(new Thread)创建,但是又想在主界面的UI线程(From1)中去操作这个窗体(Form2)的更新。例如下面的代码,应该怎么改更合适呢?

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{Form2 form2 = null;Task.Factory.StartNew(() =>{form2 = new Form2();form2.ShowDialog();});Thread.Sleep(1000);//确保form2被实例化了form2.Text = "新窗体";//会报跨线程访问的错误
}

按前文的方法们就得在Form2中添加公共的SynchronizationContext或AsyncOperation属性,然后在form1中再去定义委托,再用form2的这个属性去传递这个委托,就会很麻烦。

WinForm中实际上已经封装了更为直接的方法,即Invoke

Invoke的注释翻译过来大概如下:

在拥有此控件的基础窗口句柄的线程上执行给定的委托。在该控件所属的同一线程上调用此方法是错误的。如果控件的句柄尚不存在,这将跟随控件的父链,直到找到确实具有窗口句柄的控件或窗体。如果找不到合适的句柄,Invoke将引发异常。在调用期间引发的异常将被传递回调用方。

从任何线程都可以安全地调用控件上的五个函数:GetInvokeRequired、Invoke、BeginInvoke、EndInvoke和CreateGraphics。对于所有其他方法调用,应使用其中一个Invoke方法来封送对控件线程的调用。

GetInvokeRequired、Invoke、BeginInvoke、EndInvoke和CreateGraphics都是System.Windows.Forms.Control下的方法。

GetInvokeRequired应该是较旧的方法,最新的与之对应的应该是InvokeRequired

InvokeRequired、Invoke、BeginInvoke、EndInvoke都是借口ISynchronizeInvoke所规定的。

Invoke

在拥有此控件的基础窗口句柄的线程上执行委托。

因此前面的例子可以直接改写为:

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{Form2 form2 = null;Task.Factory.StartNew(() =>{form2 = new Form2();form2.ShowDialog();});Thread.Sleep(1000);//确保form2被实例化了form2.Invoke(() =>{form2.Text = "新窗体"});
}

在创建控件的线程上使用Invoke会报错。并且是根据控件或控件的父级中存在的窗体控件句柄(Handle)去查找底层的消息循环线程做处理的,所以控件或其父级必须具有实例的句柄,否则会抛异常。

InvokeRequired

获取一个bool值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程以外的线程中。

在创建控件的线程中使用Invoke会报错,所以当代码比较复杂时,提前做判断还是必要的:

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{Form2 form2 = null;Task.Factory.StartNew(() =>{form2 = new Form2();form2.ShowDialog();});Thread.Sleep(1000);if (form2.InvokeRequired){form2.Invoke(() =>{form2.Text = "新窗体";});}
}

BeginInvoke

先参考下源码:

public IAsyncResult BeginInvoke(Delegate method, params Object[] args) 
{using (new MultithreadSafeCallScope()) {Control marshaler = FindMarshalingControl();return(IAsyncResult)marshaler.MarshaledInvoke(this, method, args, false);}
}

返回值是IAsyncResult类型的,有点熟悉哎,因为Task就是继承自IAsyncResult的。

BeginInvoke是异步的方法。即当需要Invoke一个比较耗时的任务时,可以考虑使用BeginInvoke,并不是要在这个方法中传入异步任务,而是它本身就是以异步方式执行。这样防止某个控件或窗体长时间的更新而对调用窗体造成假死状态。

比如:

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{Form2 form2 = null;Task.Factory.StartNew(() =>{form2 = new Form2();form2.ShowDialog();});Thread.Sleep(500);if (form2.InvokeRequired){form2.BeginInvoke(() =>{Thread.Sleep(1000);form2.Text = "新窗体";});}this.lbText.Text = "按钮事件执行完毕";
}

运行效果如下:先弹出form2窗体,然后form1主窗体中的label更新,然后form2窗体的标题才更新完毕。

看下关于BeginInvoke的官方注解

委托以异步方式调用,此方法会立即返回。 你可以从任何线程调用此方法,即使是拥有控件句柄的线程。 如果控件的句柄尚不存在,此方法将搜索控件的父链,直到找到具有窗口句柄的控件或窗体。 如果未找到适当的句柄, BeginInvoke 将引发异常。 委托方法中的异常被视为未捕获,并将发送到应用程序的未捕获异常处理程序。

这里隐藏了一些坑,控件不一定有句柄,如果按父链找到句柄就是调用BeginInvoke的窗体,这到底会怎样呢。

比如下面的操作

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{    this.lbText.BeginInvoke(() =>{this.lbText.Text = "BeginInvoke正在执行";Thread.Sleep(2000);this.lbText.Text = "BeginInvoke执行完毕";});this.lbText.Text = "按钮事件执行完毕";
}

按异步的原理,应该会先看到"按钮事件执行完毕",然后"BeginInvoke正在执行"等待2秒后看到"BeginInvoke执行完毕"。然而实际上只看到最后加一句。

也就是说BeginInvoke的时候实际上时将对应句柄的窗体控件挂起,知道异步方法执行结束后再绘制界面。label控件没有句柄,会找到父窗体的句柄在其上执行,所以即使上面的例子中,窗体中引入其他控件,最终也是等BeginInvoke的内容全部执行完后才更新整个界面。

所以在使用BeginInvoke时还是要多注意,尽量是跨窗体使用。

EndInvoke

BeginInvoke官方注解中的另一段话是这个:

可以调用 EndInvoke 以从委托中检索返回值(如果为 neccesary),但这不是必需的。 EndInvoke 将阻止,直到可以检索返回值。

即EndInvoke是将异步边同步,类似于Task的Wait()方法。

使用方式是将BeginInvoke返回的IAsyncResult对象传入。

private void btnAsyncUpdate_Click(object sender, EventArgs e)
{Form2 form2 = null;Task.Factory.StartNew(() =>{form2 = new Form2();form2.ShowDialog();});Thread.Sleep(500);if (form2.InvokeRequired){IAsyncResult asyncResult = form2.BeginInvoke(() =>{Thread.Sleep(1000);form2.Text = "新窗体";});form2.EndInvoke(asyncResult);}this.lbText.Text = "按钮事件执行完毕";
}

2、死锁

2.1、问题

死锁是擦winform等界面编程中比较常见又很诡异的情况。

前文的例子中,在form1中加一个按钮(btnAsyncFunc)

按钮事件如下:

private void btnAsyncFunc_Click(object sender, EventArgs e)
{AsyncFunc().Wait();this.lbText.Text = "AsyncFunc执行完毕";
}
private async Task AsyncFunc()
{await Task.Run(() =>{Thread.Sleep(100);});
}

看似再简单不过的一段代码了,但是点击按钮时,界面会完美卡死,无法操作。

为什么?

参考之前的笔记:「C#」异步编程玩法笔记-async、await_Raink_LH的博客-CSDN博客

里面有说明async和await的执行顺序。异步方法AsyncFunc会同步运行到await处,然后运行Task并把Task返回,返回后发现是Wait(),就得等待Task执行完毕,Task执行完毕后,Task语句之后的代码是同步执行的,这里的同步执行是在创建Task的线程,例子中也就是UI线程。而UI线程只有一个,此时线程中正在运行的是调用方的Wait()方法,Wait()没有执行完毕就不会运行到Task之后的语句,所以AsyncFunc方法中Task.Run(...);之后的代码不会执行(虽然末尾没有代码了,但方法末尾的后大括号也可以认为是代码),但是AsyncFunc方法不执行到后大括号,AsyncFunc方法就不能结束,从而不能让Wait()结束。互相等,从而死锁。

由此也可以知道,在调用异步方法时使用Wait()、Result等阻塞方法时都有可能出现这种情况。

但这样的死锁不会在控制台程序中出现。且最大并发数没有做限制,await之后的代码与Wait()/Result的执行是在不同线程上发生的,两者是可以同时运行的,所以不会有影响。比如这个就不会有问题。

static void Main(string[] args)
{LockTest();Console.WriteLine("程序结束");Console.ReadLine();
}
private static void LockTest()
{Console.WriteLine("调用并等待异步方法");ConeoleAsyncFunc().Wait();Console.WriteLine("异步方法结束");
}
private static async Task ConeoleAsyncFunc()
{await Task.Run(() =>{Thread.Sleep(100);Console.WriteLine("ConeoleAsyncFunc"); });
}

但如果我们使用自定义的任务调度器,限定最大并发数为1,且拒绝内联的方式执行任务(重写TaskScheduler中的TryExecuteTaskInline方法直接return false),如下,程序就会自锁卡住。

static void Main(string[] args)
{TaskScheduler scheduler = new LimitedConcurrencyTaskScheduler(1);TaskFactory factory = new TaskFactory(scheduler);var t = factory.StartNew(LockTest);t.Wait();Console.WriteLine("程序结束");Console.ReadLine();
}private static void LockTest()
{Console.WriteLine("调用并等待异步方法");ConeoleAsyncFunc().Wait();Console.WriteLine("异步方法结束");
}
private static async Task ConeoleAsyncFunc()
{await Task.Run(() =>{Thread.Sleep(100);Console.WriteLine("ConeoleAsyncFunc"); });
}

2.2、 解决方法

2.2.1、不要await

既然要Wait(),要阻塞,那就最好把原方法改成同步的,不要有async,不要有await。

private void btnAsyncFunc_Click(object sender, EventArgs e)
{AsyncFunc().Wait();this.lbText.Text = "AsyncFunc执行完毕";
}
private Task AsyncFunc()
{var t =Task.Run(() =>{Thread.Sleep(100);});t.Wait();
}

嗯..........我承认这样有点脱裤子放屁,总之意思就是如果需要阻塞,就尽量不要用异步方法。

2.2.2、用await代替Wait()/Result

既然必须要异步,那么就异步到底,异步方法的调用者也使用async await。即:

private async void btnAsyncFunc_Click(object sender, EventArgs e)
{await AsyncFunc();this.lbText.Text = "AsyncFunc执行完毕";
}
private async Task AsyncFunc()
{await Task.Run(() =>{Thread.Sleep(100);});
}

对于异步方法有返回值TResult的,也已用await

private async void btnAsyncFunc_Click(object sender, EventArgs e)
{int num = await AsyncFunc();this.lbText.Text = "AsyncFunc执行完毕";
}
private async Task<int> AsyncFunc()
{await Task.Run(() =>{Thread.Sleep(100);});return 0;
}

2.2.3、使用新的异步方法中转

即再加一个异步方法,新的异步方法像2.2.1中说的,不要用async、await,而完全用Wait()/Result

private void btnAsyncFunc_Click(object sender, EventArgs e)
{RunAsyncFunc();this.lbText.Text = "AsyncFunc执行完毕";
}
private void RunAsyncFunc()
{var t = Task.Run(() =>{AsyncFunc().Wait();});t.Wait();
}
private async Task AsyncFunc()
{await Task.Run(() =>{Thread.Sleep(100);int a = 0;});
}

虽然异步方法AsyncFunc()后面还是用了Wait(),但是这个Wait()是在另一个线程中发生的,即AsyncFunc()中await之后的代码是在另一个线程中发生,而不是界面的UI主线程,所以不会造成死锁。

2.2.4、ConfigAwaiter(false)

这个是Task的一个公共方法。官方的注解如下:

异步方法直接等待 Task 时,延续任务通常会出现在创建任务的同一线程中,具体取决于异步上下文。 此行为可能会降低性能,并且可能会导致 UI 线程发生死锁。 若要避免这些问题,请调用 Task.ConfigureAwait(false)。

也就是说,ConfigAwaiter()传入false时,是不要将延续任务安排在创建任务的同一线程中。

按照第一节中的问题和本节死锁的问题分析,死锁根源是await结束后返回了UI线程,UI线程由呗占用。

所以如果将await之后的续接任务,安排在别的线程中,就不会死锁了。

比如这样

private async void btnAsyncFunc_Click(object sender, EventArgs e)
{int num = await AsyncFunc();this.lbText.Text = "AsyncFunc执行完毕";
}
private async Task<int> AsyncFunc()
{await Task.Run(() =>{Thread.Sleep(100);}).ConfigAwaiter(false);//使用ConfigAwaiter(false)return 0;
}

也可以解决死锁的问题。

3、ConfigAwaiter(false)

为什么把这个方法单独又列出来作为一节内容呢。再看一眼官方注解:

异步方法直接等待 Task 时,延续任务通常会出现在创建任务的同一线程中,具体取决于异步上下文。 此行为可能会降低性能,并且可能会导致 UI 线程发生死锁。 若要避免这些问题,请调用 Task.ConfigureAwait(false)。

似乎在UI编程中基于任务的多线程处理都应添加这个方法以避免死锁。

然后无脑使用这个方法,在避免第二节的死锁问题时,就很容易引发第一节的跨线程调用UI的错误。

ConfigureAwait(false)之后,也就是await的后续任务代码会在Task的上下文中执行,而如果后续任务是操作UI空间,则会触发“线程间操作无效……”的异常。

比如将上面的示例稍作调整,如下:

private void btnAsyncFunc_Click(object sender, EventArgs e)
{AsyncFunc().Wait();
}
private async Task AsyncFunc()
{await Task.Run(() =>{Thread.Sleep(100);int a = 0;}).ConfigureAwait(false);//异步完成后更新界面this.lbText.Text = "AsyncFunc执行完毕";}
}

运行后就是这样的结果。

所以ConfigAwaiter(false)不能无脑用,使用时一定主要,其后面不能有对UI界面的操作。


http://www.ppmy.cn/news/4550.html

相关文章

OWASP API安全Top 10

文章目录API1-失效的对象级授权API2-失效的用户认证API3-过度的数据暴露API4-缺乏资源和速率控制API5-失效的功能级授权API6-批量分配API7-安全性配置错误API8-注入API9-资产管理不当API10-日志记录和监控不足在API安全发展的过程中&#xff0c;除了各大安全厂商和头部互联网企…

Windows系统编译Wireshark

编译环境 操作系统Windows 10 Wireshark版本3.6.10或3.0.0 Qt版本5.15.2或5.12.12 Python版本3.8 cmake版本3.19.2(64位) Strawberry版本5.22 安装 Microsoft Visual Studio 2019 Visual Studio 2019 版本 16.11 发行说明 | Microsoft Learn 安装 Microsoft Visual S…

Java 教程

Java 教程 Java 是由 Sun Microsystems 公司于 1995 年 5 月推出的高级程序设计语言。 Java 可运行于多个平台&#xff0c;如 Windows, Mac OS 及其他多种 UNIX 版本的系统。 本教程通过简单的实例将让大家更好的了解 Java 编程语言。 移动操作系统 Android 大部分的代码采用…

C++PrimerPlus 第七章 函数-C++的编程模块(复习题)

1、使用函数的3个步骤是什么&#xff1f; 2、请创建与下面的描述匹配的函数原型。 a. igor()没有参数&#xff0c;且没有返回值。 b. tofu()接受一个int参数&#xff0c;并返回一个float。 c. mpg()接受两个double参数&#xff0c;并返回一个double。 d. summation()将long数组…

根据经纬度点,半径画一个圆

1 需求 已知圆的坐标&#xff0c;半径长度&#xff0c;单位是米&#xff0c;得到一个圆 2 解决方案 2.1 Java 语言 <dependency><groupId>com.esri.geometry</groupId><artifactId>esri-geometry-api</artifactId><version>1.1</versi…

C/C++KTV点歌系统

C/CKTV点歌系统 KTV点歌系统&#xff08;版本1&#xff09; 1 设计要求 采用链表(系统中可以设定任意数目的记录&#xff0c;但难度较大)或者结构体数组(只能限定一定数目的记录)完成系统。系统要求设计一个卡拉ok点歌系统&#xff0c; 可以显示、查询、点歌等操作。 2 系统…

242. 一个简单的整数问题——差分思想+树状数组

给定长度为 N 的数列 A&#xff0c;然后输入 M 行操作指令。 第一类指令形如 C l r d&#xff0c;表示把数列中第 l∼r 个数都加 d。 第二类指令形如 Q x&#xff0c;表示询问数列中第 x 个数的值。 对于每个询问&#xff0c;输出一个整数表示答案。 输入格式 第一行包含两…

占道经营识别检测系统 yolov5架构

占道经营识别检测系统基于opencvpython 网络架构模型对现场画面中占道经营违规摆摊行为进行实时监测预警。YOLO算法- YOLO算法是一种基于回归的算法&#xff0c;它不是选择图像中有趣的部分&#xff0c;而是预测整个图像中的类和包围框运行一次算法。要理解YOLO算法&#xff0c…