C# async/await 异步编程深入解析
2017.01.18
C#
3 min
1.3k 字
// 目录 · contents
1. async/await 的本质:状态机 2. ConfigureAwait(false) 3. 最危险的陷阱:async
与同步代码混用导致死锁 4. 并行执行多个异步操作 5.
取消异步操作:CancellationToken 6. 异步流(.NET Core 3.0 预告) 总结
async/await 是 C# 5.0
引入的特性,经过两年实际使用,我发现大多数人只是把它当成”让代码不阻塞”的语法糖来用,对底层机制不甚了解,导致写出的异步代码要么有性能问题,要么在某些情况下死锁。这篇文章是我把这块知识系统梳理之后的总结。
1. async/await 的本质:状态机
编译器会把 async 方法转换成一个状态机:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public async Task<string > GetUserNameAsync (int userId ) { var user = await _db.Users.FindAsync(userId); var profile = await _api.GetProfileAsync(user.Email); return profile.DisplayName; }public Task<string > GetUserNameAsync (int userId ) { var stateMachine = new GetUserNameAsyncStateMachine { _this = this , userId = userId, _state = -1 }; stateMachine.MoveNext(); return stateMachine._builder.Task; }private struct GetUserNameAsyncStateMachine : IAsyncStateMachine { public int _state; public AsyncTaskMethodBuilder<string > _builder; public void MoveNext () { switch (_state) { case -1 : var awaiter1 = _db.Users.FindAsync(userId).GetAwaiter(); if (!awaiter1.IsCompleted) { _state = 0 ; awaiter1.OnCompleted(MoveNext); return ; } goto case 0 ; case 0 : user = awaiter1.GetResult(); } } }
理解状态机让你明白:await
不是创建新线程,而是注册回调后释放当前线程 。
这是 async 代码里最重要也最容易忽略的细节:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public async Task LoadDataAsync () { var data = await _service.FetchDataAsync(); textBox.Text = data.Name; }public async Task<string > FetchDataAsync () { var response = await httpClient.GetStringAsync(url).ConfigureAwait(false ); return Process(response); }
库代码应该总是用
ConfigureAwait(false) ,原因: 1.
提高性能(避免无谓的线程切换) 2. 避免死锁(见下面的死锁陷阱)
3. 最危险的陷阱:async
与同步代码混用导致死锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public ActionResult DeadlockDemo () { var result = GetDataAsync().Result; return Content(result); }public async Task<string > GetDataAsync () { var data = await httpClient.GetStringAsync(url); return data; }public async Task<ActionResult> FixedDemo () { var result = await GetDataAsync(); return Content(result); }public async Task<string > GetDataAsync () { var data = await httpClient.GetStringAsync(url).ConfigureAwait(false ); return data; }
黄金法则:async
具有传染性,一旦开始用就要贯穿到底,不要在异步代码中混用
.Result/.Wait()。
4. 并行执行多个异步操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var user = await GetUserAsync(userId);var orders = await GetOrdersAsync(userId);var profile = await GetProfileAsync(userId);var userTask = GetUserAsync(userId);var ordersTask = GetOrdersAsync(userId);var profileTask = GetProfileAsync(userId);await Task.WhenAll(userTask, ordersTask, profileTask);var user = await userTask;var orders = await ordersTask;var profile = await profileTask;
注意 :Task.WhenAll
失败时会抛出第一个异常,如果需要获取所有异常:
1 2 3 4 5 6 7 8 var tasks = new [] { Task1(), Task2(), Task3() };var results = await Task.WhenAll(tasks);foreach (var task in tasks) { if (task.IsFaulted) Console.WriteLine(task.Exception); }
5.
取消异步操作:CancellationToken
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public async Task<List<User>> SearchUsersAsync( string keyword, CancellationToken cancellationToken = default ) { cancellationToken.ThrowIfCancellationRequested(); var response = await httpClient.GetAsync( $"/api/users?q={keyword} " , cancellationToken); var json = await response.Content.ReadAsStringAsync(); return JsonConvert.DeserializeObject<List<User>>(json); }using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10 ));try { var users = await SearchUsersAsync("alice" , cts.Token); }catch (OperationCanceledException) { Console.WriteLine("搜索超时" ); }
6. 异步流(.NET Core 3.0 预告)
虽然当时还是 2017
年,但值得提前了解:IAsyncEnumerable<T>
可以实现真正的异步流式处理:
1 2 3 4 5 6 7 8 public async IAsyncEnumerable<User> GetUsersStreamAsync () { await foreach (var batch in _db.Users.AsAsyncEnumerable()) { yield return batch; } }
总结
async/await 核心要点:
本质是状态机 ,不是新线程,而是释放线程等待 I/O
库代码加
ConfigureAwait(false) ,应用代码按需决定
禁止混用
.Result/.Wait() ,在有
SynchronizationContext 的环境下必然死锁
并行任务用 Task.WhenAll ,不要串行
await 独立任务
CancellationToken
贯穿始终 ,让调用方有能力取消长时间操作
async/await
语法简单,但正确使用需要理解底层机制。这些坑我基本都亲自踩过,希望这篇文章能帮你少走弯路。