为什么要用async、await ?

为什么要用async、await?

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

一、为什么要用async、await?

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

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

当请求执行到异步任务方法时,当前请求线程会释放回收到线程池,不会一直等待异步任务执行完成,可以提高线程利用率。

[HttpGet("async")] public async Task<IActionResult> GetUserAsync(int id) {     // 1. 执行一些同步逻辑...     // 2. 调用【异步】数据库查询方法 - 这会释放当前线程     var user = await _userRepository.GetAsync(id); // 假设此操作耗时9s     // 3. 对结果进行加工处理...     return Ok(user); } 

以上是一个简单的异步查询数据的方法,执行过程中线程的使用:

  1. 接收到请求后,线程池分配空闲线程线程【1】执行
  2. 线程【1】执行一些同步逻辑
  3. 线程【1】调用GetAsync方法
  4. 发送请求到数据库,线程【1】线程释放到线程池,可以继续处理其他请求
  5. 数据库相关操作由操作系统和硬件处理
  6. 数据库处理完成后,由I/O完成端口触发回调
  7. 从线程池中重新分配线程【2】继续处理后续逻辑

上述第5步耗时比较长的主要是I/O操作,用异步方法会将线程释放,等到数据库返回结果后,再从线程池中重新分配线程继续执行后续代码。如果是同步方法,那么在第4步时,线程不会被释放,一直会被占用。

那么这种差异对并发量有什么影响?假设线程池有100个线程:

【同步执行】 100线程数 / 10s 约等于每秒处理10个请求,当请求量高于这个数值时,会发生请求等待的情况。

模拟同步并发,100个请求,每个耗时10s,同时最多只有10个任务在执行,最后执行完101551ms。

static void Main(string[] args) {     int maxWorkerThreads, maxIOThreads;     ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxIOThreads);      // 设置最大工作线程数为 10     ThreadPool.SetMaxThreads(10, maxIOThreads);      var stopwatch = Stopwatch.StartNew();     Console.WriteLine($"主线程ID: {Environment.CurrentManagedThreadId}");     Console.WriteLine($"开始模拟100个耗时同步任务...n");      // 创建100个任务     var tasks = new Task[100];     for (int i = 0; i < 100; i++)     {         int taskId = i; // 捕获循环变量         tasks[i] = Task.Run(() =>         {             ExecuteSynchronousTask(taskId);         });     }      // 同步等待所有任务完成(会阻塞主线程)     Task.WaitAll(tasks);      stopwatch.Stop();     Console.WriteLine($"n所有100个同步任务完成!");     Console.WriteLine($"总耗时: {stopwatch.ElapsedMilliseconds} ms");     Console.WriteLine($"最后线程ID: {Environment.CurrentManagedThreadId}"); }  // 模拟耗时同步任务 static void ExecuteSynchronousTask(int taskId) {     Console.WriteLine($"任务 {taskId} 开始执行,线程ID: {Environment.CurrentManagedThreadId}");     Thread.Sleep(10000); // 同步延迟10秒     Console.WriteLine($"任务 {taskId} 完成,线程ID: {Environment.CurrentManagedThreadId}"); } 

为什么要用async、await ?

【异步执行】 当请求执行到数据库处理时,线程会释放到线程池,这样线程就可以用来处理其他请求。

模拟异步并发,100个请求,每个耗时10s,最后执行完10865ms。

static async Task Main(string[] args) {     int maxWorkerThreads, maxIOThreads;     ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxIOThreads);      // 设置最大工作线程数为 10     ThreadPool.SetMaxThreads(10, maxIOThreads);      var stopwatch = Stopwatch.StartNew();     Console.WriteLine($"主线程ID: {Environment.CurrentManagedThreadId}");     Console.WriteLine($"开始模拟100个耗时异步任务...");      // 创建100个任务     var tasks = new Task[100];     for (int i = 0; i < 100; i++)     {         int taskId = i; // 捕获循环变量         tasks[i] = ExecuteAsynchronousTaskAsync(taskId);     }      // 同步等待所有任务完成(会阻塞主线程)     Task.WaitAll(tasks);      stopwatch.Stop();     Console.WriteLine($"所有100个异步任务完成!");     Console.WriteLine($"总耗时: {stopwatch.ElapsedMilliseconds} ms");     Console.WriteLine($"最后线程ID: {Environment.CurrentManagedThreadId}"); }  /// <summary> /// 模拟异步耗时操作 /// </summary> /// <param name="taskId"></param> static async Task ExecuteAsynchronousTaskAsync(int taskId) {     Console.WriteLine($"任务 {taskId} 开始执行,线程ID: {Environment.CurrentManagedThreadId}");     await Task.Delay(10000);     Console.WriteLine($"任务 {taskId} 完成,线程ID: {Environment.CurrentManagedThreadId}"); } 

为什么要用async、await ?

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

不一定。上文的例子中,提高吞吐量的关键是在执行查询数据库时会释放线程。因为查询数据是I/O操作,由操作系统处理,如果是CPU类型操作时,请求线程释放后会立即分配一个新的线程处理,这样并不会提高线程利用率,反而因为线程切换增加额外的开销

所以,如果接口请求中有CPU密集型任务,我们用Task.Run限制并发量的方式,或者交给其他的服务(如消息队列)去处理。

I/O类型:数据库操作、网络请求、文件读写

CPU计算类型:图片处理、函数计算

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

不会。异步编程是通过提高线程的利用率来增加应用并发量,针对单次请求,异步操作反而会因为线程切换增加额外的开销。如果想提高单次请求速度,是通过并发编程实现。

异步编程示例:总耗时15s

static async Task Main(string[] args) {     var stopwatch = Stopwatch.StartNew();      await Task.Delay(5000);     await Task.Delay(5000);     await Task.Delay(5000);      stopwatch.Stop();     Console.WriteLine($"总耗时: {stopwatch.ElapsedMilliseconds} ms"); } 

并发编程示例:总耗时 5s

static async Task Main(string[] args) {     var stopwatch = Stopwatch.StartNew();      var task1 = Task.Delay(5000);     var task2 = Task.Delay(5000);     var task3 = Task.Delay(5000);      Task.WaitAll(task1, task2, task3);      stopwatch.Stop();     Console.WriteLine($"总耗时: {stopwatch.ElapsedMilliseconds} ms"); } 

五、async/await是如何切换线程提高吞吐量的?

通过状态机模式。
下面是一个简单的异步方法,生成后用dnSpy看下IL代码发现编译器会针对RunTasksAsync生成一个实现IAsyncStateMachine的状态机类,里面有状态记录执行进度,当异步任务执行完成时,通过回调通知继续执行方法内后续逻辑,看通过deepseek生成IL的伪代码,可以清晰了了解内部执行过程。

C#方法 static async Task RunTasksAsync(int i) {     int j = i * 10;     int result = await Task.Run(() => { return j + 1; } );     int k = result * 10;     Console.WriteLine(k); }  
IL代码通过deepseek简化 // RunTasksAsync方法的状态机   [CompilerGenerated] private sealed class <RunTasksAsync>d__1 : IAsyncStateMachine {     public int <>1__state;                    // 状态字段:控制执行流程。-1=初始/恢复,0=在await暂停,-2=已完成     public AsyncTaskMethodBuilder <>t__builder; // 构建并管理最终返回的Task对象,用于设置结果或异常     public int i;                             // 原始方法参数i,被“提升”为状态机字段,以便跨await恢复时仍可访问     private <>c__DisplayClass1_0 <>8__1;      // 显示类实例,用于捕获lambda表达式中的变量(如j=i*10),实现闭包     private int <result>5__2;                 // 对应原始代码中的局部变量'result',保存await的返回值     private int <k>5__3;                      // 对应原始代码中的局部变量'k',用于后续计算     private int <>s__4;                       // 临时字段,用于存储await表达式的结果(即awaiter.GetResult()的返回值)     private TaskAwaiter<int> <>u__1;          // 保存Task<int>的等待器(awaiter),在await未完成时暂存,恢复时使用      void MoveNext()     {         try         {             TaskAwaiter<int> awaiter;         // 局部变量:用于操作await的awaiter对象             int state = this.<>1__state;      // 读取当前状态,决定从哪开始执行                          if (state != 0) // 首次执行(state为-1)或从异常后继续,进入同步执行路径             {                 // 创建显示类来捕获lambda表达式的上下文                 this.<>8__1 = new <>c__DisplayClass1_0();  // 实例化闭包类,用于在异步lambda中访问外部变量                 this.<>8__1.j = this.i * 10;               // 在闭包类中计算 j = i * 10,供lambda使用                                  // 开始异步操作:Task.Run(() => j + 1)                 awaiter = Task.Run<int>(this.<>8__1.<RunTasksAsync>b__0).GetAwaiter();                 // 获取Task<int>的awaiter,以便检查完成状态和获取结果                                  if (!awaiter.IsCompleted)     // 如果任务尚未完成,则需要“暂停”当前方法                 {                     this.<>1__state = 0;      // 设置状态为0,表示在第一个await处暂停                     this.<>u__1 = awaiter;    // 保存awaiter到字段,供恢复时使用(避免栈变量丢失)                     this.<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);                     // 注册回调:当Task完成时,调用状态机的MoveNext()继续执行                     // 此调用会“挂起”当前线程,将控制权返回给调用者,实现非阻塞                     return;                   // 退出MoveNext,等待Task完成后再恢复                 }                 // 如果任务已同步完成(IsCompleted == true),则直接继续执行,不暂停             }             else // state == 0,表示从await暂停点恢复执行             {                 awaiter = this.<>u__1;        // 从字段中取出之前保存的awaiter                 this.<>u__1 = default;        // 清空字段,避免内存泄漏和重复使用                 this.<>1__state = -1;         // 重置状态为-1,表示正在恢复执行             }                          // 执行到此处,说明await已完成,获取结果             // 对应原始代码:int result = await Task.Run(() => j + 1);             this.<>s__4 = awaiter.GetResult();   // 调用GetResult()获取Task的返回值(int)             this.<result>5__2 = this.<>s__4;     // 将结果赋值给局部变量result(提升为字段)                          // 继续执行await之后的同步代码             this.<k>5__3 = this.<result>5__2 * 10; // 对应:k = result * 10             Console.WriteLine(this.<k>5__3);       // 对应:Console.WriteLine(k)                          // 方法正常执行完成             this.<>1__state = -2;              // 设置状态为-2,表示已完成             this.<>8__1 = null;                // 清理闭包类实例,帮助GC回收             this.<>t__builder.SetResult();     // 通知builder:Task已完成,设置为成功状态         }         catch (Exception ex)                   // 捕获await期间或后续代码中抛出的任何异常         {             this.<>1__state = -2;              // 标记为已完成(失败)             this.<>8__1 = null;                // 清理资源             this.<>t__builder.SetException(ex); // 通知builder:Task失败,设置异常             // 异常会被封装到返回的Task中,调用者通过await或Task.Exception获取         }     } }  

结尾

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

发表评论

评论已关闭。

相关文章