雷达智富

首页 > 内容 > 程序笔记 > 正文

程序笔记

C#中await/async异步编程采坑—async方法可能会同步执行

2024-08-28 42

前言

在C# 5.0和.NET 4.5中,引入了基于await/async的异步编程模式,也称为“基于任务的异步编程模型 (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方法一定就会异步执行而不会阻塞线程吗?
答案是否定的!!!
为了解释async方法如何阻塞线程,下面我们构造一些场景来看这个问题。

同步执行场景一

考虑下面的FakeDelayAsync方法

async void FakeDelayAsync()
{
    Delay();
}

以上代码中,使用async关键字标记上了FakeDelayAsync方法,但内部实现却是一个同步的Delay方法。
调用这个方法时,会有两种可能:

  • ×系统将Delay方法插入线程池执行,当前线程不会被阻塞,因为这个方法是async方法。
  • √系统直接调用Delay,当前线程阻塞。
    实际执行一下,会发现执行FakeDelayAsync和直接执行Delay的表现是一样的,两者都是同步执行。实际上,在VisualStudio中打出这个方法时,就会有警告:

此异步方法缺少 “await” 运算符,将以同步方式运行。请考虑使用 “await” 运算符等待非阻止的 API 调用,或者使用 “await Task.Run(…)” 在后台线程上执行占用大量 CPU 的工作。

同步执行场景二

考虑下面的FakeDelayAsync2和FakeDelayAsync3方法

async Task FakeDelayAsync2()
{
    Delay();
}

async void FakeDelayAsync3()
{
    await FakeDelayAsync2();
}

在FakeDelayAsync3方法中,VisualStudio没有任何警告,那么在调用这个方法时,会有两种可能:

  • ×系统将FakeDelayAsync2方法插入线程池执行,当前线程不会被阻塞,因为这个方法是async Task。
  • √系统直接调用Delay,当前线程阻塞。
    实际执行以上代码,就会发现FakeDelayAsync3和Delay的表现还是一样的。即便VisualStudio在FakeDelayAsync3中没有给出任何的警告,但它调用FakeDelayAsync2方法仍然是同步执行的!!!

同步执行场景三

也许你会认为,FakeDelayAsync3中虽然没有警告,但FakeDelayAsync2中也有啊!那么,考虑下面的情形:

async Task HalfFakeDelayAsync(bool isAsync)
{
    if (isAsync)
    {
        Delay();
    }
    else
    {
        await Task.Delay(100);
    }
}

async void FakeDelayAsync4()
{
    await HalfFakeDelayAsync(true);
}

构建一个存在await关键字的HalfFakeDelayAsync方法,再在FakeDelayAsync4中调用它。
那么,调用FakeDelayAsync4方法时,即便VisualStudio完全没有给出任何警告,但是它依然是同步执行。

同步执行场景四

你也许会认为,上面的几个场景都是别有用心的构造出来的,或是因为“错误的”编程而导致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,不论VisualStudio有没有给出警告,都有可能以同步的方式执行,从而阻塞调用线程。
  • 一个async void或者async Task究竟是否会以异步方式执行,取决于其本身内部逻辑是否会释放当前线程。
  • 因此,被标记为async的方法,仅仅是异步的一个必要不充分条件。
更新于:2个月前
赞一波!2

文章评论

评论问答