目录
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)
先新建个简单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它的时候确定的,如果在新线程中,则也会有新的同步上下文。
大致了解清楚原有后,解决这个问题的方式就明确了,无非就是两条路,一是回到创建它的线程,二是回到它的同步上下文。
先看第一种
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);
}
在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);
}
///
[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;
///
/// Constructor. Protected to avoid unwitting usage - AsyncOperation objects
/// are typically created by AsyncOperationManager calling CreateOperation.
///
private AsyncOperation(object userSuppliedState, SynchronizationContext syncContext)
{
this.userSuppliedState = userSuppliedState;
this.syncContext = syncContext;
this.alreadyCompleted = false;
this.syncContext.OperationStarted();
}
///
/// Destructor. Guarantees that sync context will always get notified of completion.
///
~AsyncOperation()
{
if (!alreadyCompleted && syncContext != null)
{
syncContext.OperationCompleted();
}
}
public object UserSuppliedState
{
get { return userSuppliedState; }
}
///
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");
}
}
///
/// Only for use by AsyncOperationManager to create new AsyncOperation objects
///
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
对象,而不是通过 SynchronizationContext 引发事件。基于任务的 API 是 .NET 中异步编程的发展方向。
如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所规定的。
在拥有此控件的基础窗口句柄的线程上执行委托。
因此前面的例子可以直接改写为:
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)去查找底层的消息循环线程做处理的,所以控件或其父级必须具有实例的句柄,否则会抛异常。
获取一个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 = "新窗体";
});
}
}
先参考下源码:
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时还是要多注意,尽量是跨窗体使用。
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 = "按钮事件执行完毕";
}
死锁是擦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");
});
}
既然要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();
}
嗯..........我承认这样有点脱裤子放屁,总之意思就是如果需要阻塞,就尽量不要用异步方法。
既然必须要异步,那么就异步到底,异步方法的调用者也使用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 AsyncFunc()
{
await Task.Run(() =>
{
Thread.Sleep(100);
});
return 0;
}
即再加一个异步方法,新的异步方法像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主线程,所以不会造成死锁。
这个是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 AsyncFunc()
{
await Task.Run(() =>
{
Thread.Sleep(100);
}).ConfigAwaiter(false);//使用ConfigAwaiter(false)
return 0;
}
也可以解决死锁的问题。
为什么把这个方法单独又列出来作为一节内容呢。再看一眼官方注解:
异步方法直接等待 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界面的操作。