异步编程场景以及最佳实践
分类: .Net技术 ◆ 标签: #.Net #异步编程 #基础 ◆ 发布于: 2023-06-04 19:01:12

前面我们学习了如何使用关键字async
以及await
和类Task
进行异步编程,并且通过一个例子演示了如何进行异步方法的设计以及优化,今天我们继续学习一下异步编程主要应用场景,以及如何应对这些场景。
在.Net中异步应用的场景主要有两种:
- 基于IO的异步编程:例如:从网络上请求数据,存取数据库,读写文件等等。
- 基于CPU密集计算的异步编程,例如很多需要大量的CPU计算的应用场景。
以上这两个应用场景是.Net的异步编程模型中非常常见的应用场景,应用.Net的异步编程模型实际上是主要是基于类Task
以及Task<T>
的泛型模型,在我们的模型中通过关键字async
以及await
来应用他们,这个在我们之前的文章中也详细的描述过了。针对以上两个场景,一般推荐的做法如下:
- 基于IO的异步应用场景直接在
async
定义的异步方法中使用await
等待IO操作,这个和我们之前文章描述的是一致的。 - 基于CPU计算的场景,建议在
async
定义的异步方法中使用await
来等待一个由Task.run
开始运行的一个方法,注意使用Task.run
会将任务放入到后台线程中去。
之前我们的文章也强调过await
的作用以及当方法调用者运行到await
代码时,CLR
会将已经在被调用的方法里的控制点返回给调用方法,从而可以使得调用方法可以不被block然后可以用于其他得任务,例如用于处理UI的线程等等,关于异步模式的设计有很多中设计模式,后面我们使用几篇学习日志来向大家介绍异步编程的设计模式。目前仅仅需要记住在.Net
中使用的时async
以及await
以及Task
来使用基于TAP
的异步编程。
实例
我们先来学习几个基于IO和基于CPU的异步编程例子。
基于IO的异步编程实例
从一个Web
服务下载数据,这个实例是一个典型的基于IO的异步编程模型,我们这个实例时在UI
的环境中,当按钮被按下,从web
服务下载数据的任务并不会block
UI的线程,可以使用如下的代码来演示:
private readonly HttpClient _httpClient = new HttpClient(); downloadButton.Clicked += async (o, e) => { // This line will yield control to the UI as the request // from the web service is happening. // // The UI thread is now free to perform other work. // 从这个await开始,控制点将会被让给UI控制线程, var stringData = await _httpClient.GetStringAsync(URL); DoSomethingWithData(stringData); };
基于CPU计算的异步编程实例
我们这里使用了一个游戏的例子,在这个例子中需要计算对游戏中的敌人进行计算,可以使用如下的代码对这个场景进行模拟,注意时一个CPU密集型的异步编程场景。
private DamageResult CalculateDamageDone() { // Code omitted: // // Does an expensive calculation and returns // the result of that calculation. } calculateButton.Clicked += async (o, e) => { // This line will yield control to the UI while CalculateDamageDone() // performs its work. The UI thread is free to perform other work. var damageResult = await Task.Run(() => CalculateDamageDone()); DisplayDamage(damageResult); };
在这个例子里我们可以看到基于CPU密集型的任务计算,我们直接使用了Task.run()
方法将需要执行的代码放入后台线程,并且不需要我们手动进行后台线程管理。
一些重要的概念
如果你对async
和await
背后的逻辑非常感兴趣,那么请多多关注我们这个公众号,这之后我们会继续学习相关的文章,例如深入async
就会详细的刨析async
和await
的背后的技术和实现,需要明确的是在C#
中,编译器会帮你处理大多数的事情,这包括CLR
会在遇到了await
的时候创建一个状态机(a State Machine
),并且使用这个状态机来跟踪每个await
的执行和恢复,例如交出控制点之后,当然await
的代码运行完成了,任然是需要找回控制点继续执行的。
从纯理论来看,async
,await
, Task
是典型的设计模式Promise Model of asynchrony
的实现,之后我们也会学习这个模式设计。
如下一些重要的概念我们时刻牢记:
- 异步代码既可以应用于基于IO的场景也可以应用于基于CPU密集计算的场景,但是在调用上是不一样的,请参考我们的例子。
- 异步代码使用
Task<T>
或者Task
作为返回值,代表后台执行的任务 - 关键字
async
将一个方法转为异步方法,同时需要在方法体中使用await
来等待一些调用。 - 当遇到关键字
await
,CLR
会立即挂起当前执行的方法,并将控制点返回到方法的调用者,一直持续到等待的任务全部完成。 await
关键字只能应用在async
方法的内部。
如何甄别基于IO的异步还是基于CPU的异步
我们前面介绍过了如何分别针对这两种应用场景来使用异步编程的模型,那么如何甄别一个场景到底是基于IO的还是基于CPU就显得非常重要:
- 你的代码是否要等待什么资源?例如从网络来的数据,从数据库中来的数据等等,如果是,那么就是基于IO的异步场景。
- 您的代码是否要运行一些非常耗时的计算?如果是,那么就是基于CPU的异步场景。
如果是基于IO的场景,使用async
和await
,不要使用Task.run
, 至于原因,我们后面会继续写文章进行分析。
如果是基于CPU的场景,使用async
和await
,同时使用Task.Run
将工作切换到一个新的后台线程上,如果您的场景非常考虑并发和并行,那么请考虑使用TPL
(Task Parallel Library
), 这里我们明确的要理解异步
和并发
, 以及并行
。
其他的例子
我们在举多一些例子
从网络下载数据
private readonly HttpClient _httpClient = new HttpClient(); [HttpGet, Route("DotNetCount")] public async Task<int> GetDotNetCount() { // Suspends GetDotNetCount() to allow the caller (the web server) // to accept another request, rather than blocking on this one. var html = await _httpClient.GetStringAsync("https://dotnetfoundation.org"); return Regex.Matches(html, @"\.NET").Count; }
等待多个Task
运行结束
public async Task<User> GetUserAsync(int userId) { // Code omitted: // // Given a user Id {userId}, retrieves a User object corresponding // to the entry in the database with {userId} as its Id. } public static async Task<IEnumerable<User>> GetUsersAsync(IEnumerable<int> userIds) { var getUserTasks = new List<Task<User>>(); foreach (int userId in userIds) { getUserTasks.Add(GetUserAsync(userId)); } return await Task.WhenAll(getUserTasks); }
或者使用我们的神器LINQ
public async Task<User> GetUserAsync(int userId) { // Code omitted: // // Given a user Id {userId}, retrieves a User object corresponding // to the entry in the database with {userId} as its Id. } public static async Task<User[]> GetUsersAsync(IEnumerable<int> userIds) { var getUserTasks = userIds.Select(id => GetUserAsync(id)); return await Task.WhenAll(getUserTasks); }
所以LINQ
的返回值也是一个Task
, 使用await
会自动返回需要的类型。只是这里有一个需要注意的地方,因为LINQ
使用延迟加载的方式,因此这里使用了Select
并不会立即执行。
最佳实践
async
方法体中至少需要一个await
,否则并不会将控制点转交给调用者。- 异步方法最好以
Async
结尾表示一个异步方法,这虽然不是强制的,但是这是一个约定俗称,建议要遵守。 async void
仅仅用在事件处理中,其他的场景请使用Task
- 在结合lambda表达式和LINQ的时候谨慎使用async, 这主要是在LINQ中延迟运行特性带来的复杂性。
- 使用await避免引发block:
- 使用
await
代替Task.Wait
和Task.Result
- 使用
await Task.WhenAny
代替Task.WhenAny
- 使用
await Task.WhenAll
代替Task.WhenAll
- 使用
await Task.Delay
代替Thread.sleep
- 使用
- 权衡和谨慎使用
ValueTask
, 后面会再写一些文章来解释这个。 - 仔细考虑使用
ConfigureAwait(false)
, 后面也会写相应的文章来描述这个部分。
关于异步编程的应用场景和介绍今天就到这里了。我们下一节再继续学习。