为什么要用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】执行一些同步逻辑
- 线程【1】调用GetAsync方法
- 发送请求到数据库,线程【1】线程释放到线程池,可以继续处理其他请求
- 数据库相关操作由操作系统和硬件处理
- 数据库处理完成后,由I/O完成端口触发回调
- 从线程池中重新分配线程【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}");
- }
复制代码
【异步执行】 当请求执行到数据库处理时,线程会释放到线程池,这样线程就可以用来处理其他请求。
模拟异步并发,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是不是一定可以提高系统的吞吐量?
不一定。上文的例子中,提高吞吐量的关键是在执行查询数据库时会释放线程。因为查询数据是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获取
- }
- }
- }
复制代码 结尾
以上是个人理解的整理,也参考了其他博主的文档,如果错误,欢迎指正,感谢
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |