找回密码
 立即注册
首页 业界区 业界 为什么要用async、await ?

为什么要用async、await ?

存叭 5 小时前
为什么要用async、await?

聊到c#基础,async/await是绕不开的话题,本文只是结合自己后端开发的经验,按照自己的思路重新整理了一下,介绍的场景也是针对webapi接口请求。

一、为什么要用async、await?

异步编程可以提高系统的吞吐量,async/await语法简化了异步编程的实现,降低使用门槛。

二、为什么异步编程可以极大地提高系统的吞吐量?

当请求执行到异步任务方法时,当前请求线程会释放回收到线程池,不会一直等待异步任务执行完成,可以提高线程利用率。
  1. [HttpGet("async")]
  2. public async Task<IActionResult> GetUserAsync(int id)
  3. {
  4.     // 1. 执行一些同步逻辑...
  5.     // 2. 调用【异步】数据库查询方法 - 这会释放当前线程
  6.     var user = await _userRepository.GetAsync(id); // 假设此操作耗时9s
  7.     // 3. 对结果进行加工处理...
  8.     return Ok(user);
  9. }
复制代码
以上是一个简单的异步查询数据的方法,执行过程中线程的使用:

  • 接收到请求后,线程池分配空闲线程线程【1】执行
  • 线程【1】执行一些同步逻辑
  • 线程【1】调用GetAsync方法
  • 发送请求到数据库,线程【1】线程释放到线程池,可以继续处理其他请求
  • 数据库相关操作由操作系统和硬件处理
  • 数据库处理完成后,由I/O完成端口触发回调
  • 从线程池中重新分配线程【2】继续处理后续逻辑
上述第5步耗时比较长的主要是I/O操作,用异步方法会将线程释放,等到数据库返回结果后,再从线程池中重新分配线程继续执行后续代码。如果是同步方法,那么在第4步时,线程不会被释放,一直会被占用。

那么这种差异对并发量有什么影响?假设线程池有100个线程:
【同步执行】 100线程数 / 10s 约等于每秒处理10个请求,当请求量高于这个数值时,会发生请求等待的情况。
模拟同步并发,100个请求,每个耗时10s,同时最多只有10个任务在执行,最后执行完101551ms。
  1. static void Main(string[] args)
  2. {
  3.     int maxWorkerThreads, maxIOThreads;
  4.     ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxIOThreads);
  5.     // 设置最大工作线程数为 10
  6.     ThreadPool.SetMaxThreads(10, maxIOThreads);
  7.     var stopwatch = Stopwatch.StartNew();
  8.     Console.WriteLine($"主线程ID: {Environment.CurrentManagedThreadId}");
  9.     Console.WriteLine($"开始模拟100个耗时同步任务...\n");
  10.     // 创建100个任务
  11.     var tasks = new Task[100];
  12.     for (int i = 0; i < 100; i++)
  13.     {
  14.         int taskId = i; // 捕获循环变量
  15.         tasks[i] = Task.Run(() =>
  16.         {
  17.             ExecuteSynchronousTask(taskId);
  18.         });
  19.     }
  20.     // 同步等待所有任务完成(会阻塞主线程)
  21.     Task.WaitAll(tasks);
  22.     stopwatch.Stop();
  23.     Console.WriteLine($"\n所有100个同步任务完成!");
  24.     Console.WriteLine($"总耗时: {stopwatch.ElapsedMilliseconds} ms");
  25.     Console.WriteLine($"最后线程ID: {Environment.CurrentManagedThreadId}");
  26. }
  27. // 模拟耗时同步任务
  28. static void ExecuteSynchronousTask(int taskId)
  29. {
  30.     Console.WriteLine($"任务 {taskId} 开始执行,线程ID: {Environment.CurrentManagedThreadId}");
  31.     Thread.Sleep(10000); // 同步延迟10秒
  32.     Console.WriteLine($"任务 {taskId} 完成,线程ID: {Environment.CurrentManagedThreadId}");
  33. }
复制代码
1.png


【异步执行】 当请求执行到数据库处理时,线程会释放到线程池,这样线程就可以用来处理其他请求。
模拟异步并发,100个请求,每个耗时10s,最后执行完10865ms。
  1. static async Task Main(string[] args)
  2. {
  3.     int maxWorkerThreads, maxIOThreads;
  4.     ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxIOThreads);
  5.     // 设置最大工作线程数为 10
  6.     ThreadPool.SetMaxThreads(10, maxIOThreads);
  7.     var stopwatch = Stopwatch.StartNew();
  8.     Console.WriteLine($"主线程ID: {Environment.CurrentManagedThreadId}");
  9.     Console.WriteLine($"开始模拟100个耗时异步任务...");
  10.     // 创建100个任务
  11.     var tasks = new Task[100];
  12.     for (int i = 0; i < 100; i++)
  13.     {
  14.         int taskId = i; // 捕获循环变量
  15.         tasks[i] = ExecuteAsynchronousTaskAsync(taskId);
  16.     }
  17.     // 同步等待所有任务完成(会阻塞主线程)
  18.     Task.WaitAll(tasks);
  19.     stopwatch.Stop();
  20.     Console.WriteLine($"所有100个异步任务完成!");
  21.     Console.WriteLine($"总耗时: {stopwatch.ElapsedMilliseconds} ms");
  22.     Console.WriteLine($"最后线程ID: {Environment.CurrentManagedThreadId}");
  23. }
  24. /// <summary>
  25. /// 模拟异步耗时操作
  26. /// </summary>
  27. /// <param name="taskId"></param>
  28. static async Task ExecuteAsynchronousTaskAsync(int taskId)
  29. {
  30.     Console.WriteLine($"任务 {taskId} 开始执行,线程ID: {Environment.CurrentManagedThreadId}");
  31.     await Task.Delay(10000);
  32.     Console.WriteLine($"任务 {taskId} 完成,线程ID: {Environment.CurrentManagedThreadId}");
  33. }
复制代码
2.png


三、async/await是不是一定可以提高系统的吞吐量?

不一定。上文的例子中,提高吞吐量的关键是在执行查询数据库时会释放线程。因为查询数据是I/O操作,由操作系统处理,如果是CPU类型操作时,请求线程释放后会立即分配一个新的线程处理,这样并不会提高线程利用率,反而因为线程切换增加额外的开销
所以,如果接口请求中有CPU密集型任务,我们用Task.Run限制并发量的方式,或者交给其他的服务(如消息队列)去处理。
I/O类型:数据库操作、网络请求、文件读写
CPU计算类型:图片处理、函数计算

四、async/await是不是可以提高单次接口的响应速度?

不会。异步编程是通过提高线程的利用率来增加应用并发量,针对单次请求,异步操作反而会因为线程切换增加额外的开销。如果想提高单次请求速度,是通过并发编程实现。
异步编程示例:总耗时15s
  1. static async Task Main(string[] args)
  2. {
  3.     var stopwatch = Stopwatch.StartNew();
  4.     await Task.Delay(5000);
  5.     await Task.Delay(5000);
  6.     await Task.Delay(5000);
  7.     stopwatch.Stop();
  8.     Console.WriteLine($"总耗时: {stopwatch.ElapsedMilliseconds} ms");
  9. }
复制代码
并发编程示例:总耗时 5s
  1. static async Task Main(string[] args)
  2. {
  3.     var stopwatch = Stopwatch.StartNew();
  4.     var task1 = Task.Delay(5000);
  5.     var task2 = Task.Delay(5000);
  6.     var task3 = Task.Delay(5000);
  7.     Task.WaitAll(task1, task2, task3);
  8.     stopwatch.Stop();
  9.     Console.WriteLine($"总耗时: {stopwatch.ElapsedMilliseconds} ms");
  10. }
复制代码
五、async/await是如何切换线程提高吞吐量的?

通过状态机模式。
下面是一个简单的异步方法,生成后用dnSpy看下IL代码发现编译器会针对RunTasksAsync生成一个实现IAsyncStateMachine的状态机类,里面有状态记录执行进度,当异步任务执行完成时,通过回调通知继续执行方法内后续逻辑,看通过deepseek生成IL的伪代码,可以清晰了了解内部执行过程。
  1. C#方法
  2. static async Task RunTasksAsync(int i)
  3. {
  4.     int j = i * 10;
  5.     int result = await Task.Run(() => { return j + 1; } );
  6.     int k = result * 10;
  7.     Console.WriteLine(k);
  8. }
复制代码
  1. IL代码通过deepseek简化
  2. // RunTasksAsync方法的状态机  
  3. [CompilerGenerated]
  4. private sealed class <RunTasksAsync>d__1 : IAsyncStateMachine
  5. {
  6.     public int <>1__state;                    // 状态字段:控制执行流程。-1=初始/恢复,0=在await暂停,-2=已完成
  7.     public AsyncTaskMethodBuilder <>t__builder; // 构建并管理最终返回的Task对象,用于设置结果或异常
  8.     public int i;                             // 原始方法参数i,被“提升”为状态机字段,以便跨await恢复时仍可访问
  9.     private <>c__DisplayClass1_0 <>8__1;      // 显示类实例,用于捕获lambda表达式中的变量(如j=i*10),实现闭包
  10.     private int <result>5__2;                 // 对应原始代码中的局部变量'result',保存await的返回值
  11.     private int <k>5__3;                      // 对应原始代码中的局部变量'k',用于后续计算
  12.     private int <>s__4;                       // 临时字段,用于存储await表达式的结果(即awaiter.GetResult()的返回值)
  13.     private TaskAwaiter<int> <>u__1;          // 保存Task<int>的等待器(awaiter),在await未完成时暂存,恢复时使用
  14.     void MoveNext()
  15.     {
  16.         try
  17.         {
  18.             TaskAwaiter<int> awaiter;         // 局部变量:用于操作await的awaiter对象
  19.             int state = this.<>1__state;      // 读取当前状态,决定从哪开始执行
  20.             
  21.             if (state != 0) // 首次执行(state为-1)或从异常后继续,进入同步执行路径
  22.             {
  23.                 // 创建显示类来捕获lambda表达式的上下文
  24.                 this.<>8__1 = new <>c__DisplayClass1_0();  // 实例化闭包类,用于在异步lambda中访问外部变量
  25.                 this.<>8__1.j = this.i * 10;               // 在闭包类中计算 j = i * 10,供lambda使用
  26.                
  27.                 // 开始异步操作:Task.Run(() => j + 1)
  28.                 awaiter = Task.Run<int>(this.<>8__1.<RunTasksAsync>b__0).GetAwaiter();
  29.                 // 获取Task<int>的awaiter,以便检查完成状态和获取结果
  30.                
  31.                 if (!awaiter.IsCompleted)     // 如果任务尚未完成,则需要“暂停”当前方法
  32.                 {
  33.                     this.<>1__state = 0;      // 设置状态为0,表示在第一个await处暂停
  34.                     this.<>u__1 = awaiter;    // 保存awaiter到字段,供恢复时使用(避免栈变量丢失)
  35.                     this.<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
  36.                     // 注册回调:当Task完成时,调用状态机的MoveNext()继续执行
  37.                     // 此调用会“挂起”当前线程,将控制权返回给调用者,实现非阻塞
  38.                     return;                   // 退出MoveNext,等待Task完成后再恢复
  39.                 }
  40.                 // 如果任务已同步完成(IsCompleted == true),则直接继续执行,不暂停
  41.             }
  42.             else // state == 0,表示从await暂停点恢复执行
  43.             {
  44.                 awaiter = this.<>u__1;        // 从字段中取出之前保存的awaiter
  45.                 this.<>u__1 = default;        // 清空字段,避免内存泄漏和重复使用
  46.                 this.<>1__state = -1;         // 重置状态为-1,表示正在恢复执行
  47.             }
  48.             
  49.             // 执行到此处,说明await已完成,获取结果
  50.             // 对应原始代码:int result = await Task.Run(() => j + 1);
  51.             this.<>s__4 = awaiter.GetResult();   // 调用GetResult()获取Task的返回值(int)
  52.             this.<result>5__2 = this.<>s__4;     // 将结果赋值给局部变量result(提升为字段)
  53.             
  54.             // 继续执行await之后的同步代码
  55.             this.<k>5__3 = this.<result>5__2 * 10; // 对应:k = result * 10
  56.             Console.WriteLine(this.<k>5__3);       // 对应:Console.WriteLine(k)
  57.             
  58.             // 方法正常执行完成
  59.             this.<>1__state = -2;              // 设置状态为-2,表示已完成
  60.             this.<>8__1 = null;                // 清理闭包类实例,帮助GC回收
  61.             this.<>t__builder.SetResult();     // 通知builder:Task已完成,设置为成功状态
  62.         }
  63.         catch (Exception ex)                   // 捕获await期间或后续代码中抛出的任何异常
  64.         {
  65.             this.<>1__state = -2;              // 标记为已完成(失败)
  66.             this.<>8__1 = null;                // 清理资源
  67.             this.<>t__builder.SetException(ex); // 通知builder:Task失败,设置异常
  68.             // 异常会被封装到返回的Task中,调用者通过await或Task.Exception获取
  69.         }
  70.     }
  71. }
复制代码
结尾

以上是个人理解的整理,也参考了其他博主的文档,如果错误,欢迎指正,感谢

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册