目录

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界面的操作。

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

  1. 「天猫代运营」精准标签玩法,引爆新品手淘搜索流量

    「天猫代运营」精准标签玩法,引爆新品手淘搜索流量 首先不管怎么样,做电商都是要讲事实,看数据的我们先看下以下几个案例,以下几个案例都是近期在操作的新品,并且都刚刚经历第一波流量爆发期 案例一:06-1 ...

  2. LibreOJ #2006. 「SCOI2015」小凸玩矩阵 二分答案+二分匹配

    #2006. 「SCOI2015」小凸玩矩阵 内存限制:256 MiB时间限制:1000 ms标准输入输出 题目类型:传统评测方式:文本比较 上传者: 匿名 提交提交记录统计讨论测试数据 题目描述 小 ...

  3. 阿里技术专家加多:Java异步编程实战之基于JDK中的Future实现异步编程 | 文末赠书...

    正文共:14244 字 8 图 预计阅读时间: 36 分钟 本节内容摘自<Java异步编程实战>中的一小节. 一.前言 本节主要讲解如何使用JDK中的Future实现异步编程,这包含如何使 ...

  4. 阿里技术专家加多:Java异步编程实战之基于JDK中的Future实现异步编程

    正文共:14244 字 8 图 预计阅读时间: 36 分钟 本节内容摘自<Java异步编程实战>中的一小节. 一.前言 本节主要讲解如何使用JDK中的Future实现异步编程,这包含如何使 ...

  5. 小学计算机课在玩中学,小鹿编程“玩中学、学中玩”趣味课程让孩子爱上学习...

    原标题:小鹿编程"玩中学.学中玩"趣味课程让孩子爱上学习 孩子们都是天真无邪的小天使,他们喜欢什么不喜欢什么都会直观的表达出来,作为家长有时候真的很"无奈",我 ...

  6. [书籍精读]《JavaScript异步编程》精读笔记分享

    写在前面 书籍介绍:本书讲述基本的异步处理技巧,包括PubSub.事件模式.Promises等,通过这些技巧,可以更好的应对大型Web应用程序的复杂性,交互快速响应的代码.理解了JavaScript的 ...

  7. Substance Painter 的一些玩法笔记

    Substance Painter 的一些玩法 1.模型边缘磨损效果: 参考连接:Substance Painter 后期添加法线贴图,处理边缘磨损!_哔哩哔哩_bilibili 使用方法: 1.导入 ...

  8. 「补课」进行时:设计模式(11)——游戏中的策略模式

    1. 前文汇总 「补课」进行时:设计模式系列 2. 游戏中的策略模式 我是一个很喜欢玩游戏的人,周末在家打打游戏是真的很开心. 回想起来当年上大学的往昔峥嵘岁月,那时候基本上是一个人在玩游戏,背后围着 ...

  9. 《亿级流量JAVA高并发与网络编程实战》笔记--------更新中

    <亿级流量JAVA高并发与网络编程实战>笔记 第一章 高并发概述 "高并发技术" 是一个广义的概念,是指一种高效的地实现并发需求的解决方案,是技术领域的名称,可以包含架 ...

最新文章

  1. mysql error log清理_手动删除mysql日志/var/log/mysql/error.log导致的mysql无法启动
  2. [深度学习-原理]浅谈Attention Model
  3. python遗传算法_基于Python的遗传算法特征约简(附代码)
  4. Android 如何在Eclipse中查看Android API源码 及 support包源码
  5. windows 下使用 MinGW + msys 编译 ffmpeg
  6. 要用“+智能”强身健体,“心脏”做好准备了吗
  7. vue.js中的路由vue-router2.0使用
  8. P5057 [CQOI2006]简单题
  9. 关于XML 编辑工具
  10. 苹果系统自带的计算机怎么恢复出厂设置,苹果电脑MacBook如何将系统恢复出厂设置...
  11. 基于强化学习工具箱的自适应巡航控制系统
  12. 饥荒无条件制作下载_饥荒巨人国无条件制造版
  13. 【网络安全学习实践】Windows基本DOS文件命令与简易病毒编写
  14. 将数据库中用户表 名称设置对应的拼音账号sql
  15. 贴一个之前写的阴阳师源码,供各位学习
  16. C语言handle对象对应java,c语言和java语言之间的关系到底是怎么样的,两种 体系?还是一种体系的不同阶段?还是其它的什么?...
  17. 商场wifi覆盖方案
  18. 海康设备NVR、IPC取rtsp流格式
  19. 卸载DirectX9.8
  20. FLY--互联网经典语录

热门文章

  1. ajax请求 下载zip压缩包
  2. PX Deq Create send blkd
  3. postman + node-red 发送e-mail(附件、抄送、密送)
  4. 【数据结构】-哈夫曼树以及哈夫曼编码
  5. 迅雷 5.8.14.706 收藏版
  6. 服务器CPU作用是什么?
  7. html 视频超出隐藏,html – 如何阻止视频标签溢出其容器div.
  8. PHP实现 美化 打赏点 金额 换算
  9. 数学建模算法与应用 线性规划(cvxpy包)
  10. 服务器更换桌面壁纸,通过VBS更换桌面设置壁纸源码