异步不是线程
2024-03-17 17:33:41

我和很多人一样,刚学着使用async方法时以为是会新开一个线程并执行代码。心想有了这玩意还要ThreadTask干嘛。。。
而实时并非如此。

线程的问题就是会造成 CPU 资源的浪费。

1
2
3
4
private void button1_Click(object sender, EventArgs e)
{
Thread.Sleep(2000); // 模拟某种耗时的操作。在此期间界面无法响应
}

由于这个方法是在 UI 线程上执行的,所以整个线程被冻结了,导致界面无法响应。在此期间该线程无事可做,这就是资源的浪费。

如果用异步就不同了:

1
2
3
4
private async void button1_Click(object sender, EventArgs e)
{
await Task.Delay(2000); // 与 Thread.Sleep 不同的是,await 不会阻塞线程
}

当遇到 await 表达式时,它会暂停当前方法的执行,并返回控制权给调用者,使得线程可以执行其他工作,比如响应 UI。
一旦可等待对象操作完成,await 表达式就会返回并继续执行后面的代码。

注意,await 后面的代码可能会与之前的代码运行在不同的线程上,这取决于SynchronizationContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static async Task TestAync()
{
// 仍然在主线程上
Debug.WriteLine($"TestAync: {Thread.CurrentThread.ManagedThreadId}");

// 不使用原始上下文。await 之后的代码可能会被分配到其他线程上执行
await Task.Delay(1).ConfigureAwait(false);

// 用 Sleep 模拟耗时操作,没有冻结 UI 线程,因为此时的线程已经切换到非 UI 线程上了
Thread.Sleep(2000);

// 与开头的线程ID不同了
Debug.WriteLine($"TestAync: {Thread.CurrentThread.ManagedThreadId}");
}

private async void button1_Click(object sender, EventArgs e)
{
await TestAync();
}

总结

异步模型解决了在此前开发中的两个痛点:

  1. 解决传统的基于回调的异步编程所带来的一系列问题。使得在异步代码中可以使用类似同步代码的方式来编写异步操作,避免了回调地狱。
  2. 更好地利用线程,await关键字可以使应用程序能够处理更多的并发操作,提高了应用程序的并发性能和吞吐量。

  • async方法可以通过返回TaskTask<T>void来标记。返回类型为TaskTask<T>的方法可以实现异步操作,并在操作完成时提供结果或状态。返回类型为void 的方法通常用于事件处理程序等不需要提供结果的情况。
  • async方法不会开启新线程。async方法本质上是一个状态机,它可以在执行过程中暂停和恢复,而不会阻塞调用线程。
  • await会让出当前线程的 CPU 时间片,以允许其他任务在可用的线程上执行。
  • await之前的代码和之后的代码可能不在同一个线程上,这取决于上下文和调度器。
  • 可以使用ConfigureAwait(false)来禁用上下文捕获,以避免不必要的线程切换。这在某些情况下可以提高性能,特别是在不需要 UI 上下文的非 UI 代码中。
  • 异步方法应该遵循一致的命名约定,以便清楚地表示其异步特性,例如在方法名后添加”Async”后缀。

参考

Async and Await
If async-await doesn’t create any additional threads, then how does it make applications responsive?