C#的await/async异步编程陷阱1:async方法并不一定会异步执行

C#中标记为async的方法一定就会异步执行而不会阻塞线程吗?答案是NO!

基于的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方法——依然是同步执行的!


总结

总结以上四个场景得到的结果,实际上,我们可以得知:

  1. 首先,不论是async void还是async Task,不论IDE有没有绿波浪警告,都有可能以同步的方法执行,从而阻塞调用线程。
  2. 一个async void或者async Task究竟是否会以异步方式执行,取决于其本身内部逻辑是否会释放当前线程

因此,被标记为async的方法,仅仅是异步的一个必要不充分条件

称谓(*)
邮箱
留言(*)