基于的await/async异步编程模式在C# 5.0和.NET 4.5中引入,也被称为“基于任务的异步编程模型 (TAP) ”。它有效地避免了异步任务回调嵌套的地狱,而且非常易于使用,但是对它深度理解却比学会使用它困难得多。
await/async的异步方法通常会被安插到线程池中运行,也可以设置为启动新的线程执行;总之,它一般不会阻塞当前调用的线程,例如:
async void DelayAsync()
{
await Task.Delay(100);
}
void Delay()
{
Thread.Sleep(100);
}
我们把Delay看作某个耗时的方法,而DelayAsync视作它的异步版本,那么,我在一个窗体程序中分别调用这两个方法:
private void button1_Click(object sender, EventArgs e)
{
for (int i = 0; i < 50; i++)
{
DelayAsync();
}
}
private void button2_Click(object sender, EventArgs e)
{
for (int i = 0; i < 50; i++)
{
Delay();
}
}
就会发现,点击button2时UI线程阻塞了5秒钟,而点击button1时则完全没有异样。那么——
async方法一定就会异步执行而不会阻塞线程吗?
答案是NO!
为了解释原因,下面我们精心构造一些场景来看这个问题。
场景一
考虑下面的FakeDelayAsync方法
async void FakeDelayAsync()
{
Delay();
}
FakeDelayAsync方法被标记上了async关键字,但内部实现却是一个同步的Delay方法。调用这个方法时,先验来说,会有两种可能:
×系统将Delay方法插入线程池执行,当前线程不会被阻塞,因为这个方法是async方法。
√系统直接调用Delay,当前线程阻塞。
——实际执行一下,就会发现FakeDelayAsync和Delay的表现是一样的,两者都是同步执行。实际上,在IDE中打出这个方法时,就会有警告:
说的正是这个意思。
场景二
考虑下面的FakeDelayAsync2和FakeDelayAsync3方法
async Task FakeDelayAsync2()
{
Delay();
}
async void FakeDelayAsync3()
{
await FakeDelayAsync2();
}
在FakeDelayAsync3方法中,IDE没有任何警告,那么在调用这个方法时,先验来说,会有两种可能:
×系统将FakeDelayAsync2方法插入线程池执行,当前线程不会被阻塞,因为这个方法是async Task。
√系统直接调用Delay,当前线程阻塞。
——实际执行一下,就会发现FakeDelayAsync3和Delay的表现还是一样的。即便IDE在FakeDelayAsync3中没有给出任何的警告,但它调用FakeDelayAsync2方法是完完全全地同步执行的!
场景三
你也许会认为,FakeDelayAsync3中虽然没有警告,但FakeDelayAsync2中也有啊!那么,考虑下面的情形:
async Task HalfFakeDelayAsync(bool isAsync)
{
if (isAsync)
{
Delay();
}
else
{
await Task.Delay(100);
}
}
async void FakeDelayAsync4()
{
await HalfFakeDelayAsync(false);
}
构建一个确实存在await关键字的HalfFakeDelayAsync方法,再在FakeDelayAsync4中调用它。
那么,调用FakeDelayAsync4方法时,即便IDE全程没有给出任何的警告,它依然是同步执行。
场景四
你也许会认为,上面的场景都是别有用心构造出来的,或是因为“错误的”编程而导致async方法被同步执行的,那么,我们可以考察下面这个完全使用.NET Framework所提供async方法的例子:
async void FakeDelayAsync5()
{
SemaphoreSlim semaphore = new SemaphoreSlim(int.MaxValue);
for (int i = 0; i < 10000000; i++)
{
await semaphore.WaitAsync();
}
}
这个FakeDelayAsync5方法——依然是同步执行的!
总结
总结以上四个场景得到的结果,实际上,我们可以得知:
- 首先,不论是async void还是async Task,不论IDE有没有绿波浪警告,都有可能以同步的方法执行,从而阻塞调用线程。
- 一个async void或者async Task究竟是否会以异步方式执行,取决于其本身内部逻辑是否会释放当前线程。
因此,被标记为async的方法,仅仅是异步的一个必要不充分条件。