异步的概念
参考:https://learn.microsoft.com/zh-cn/dotnet/csharp/asynchronous-programming/
以做早餐的场景为例,现在有以下几个需要做步骤:
- 倒一杯咖啡
- 热锅,然后煎两个鸡蛋
- 煎三片培根
- 烤两片面包
- 在吐司中加入黄油和果酱
- 倒入一杯橙汁
在具体执行中,我们不需要等待上一件事完成才开始做另一件事(这是同步的方法)。
开始热锅的时候,可以去开始煎培根,开始煎培根,可以开始烤面包;煎培根(第三个任务)不需要等待热锅,两个鸡蛋都煎好(等待第二个任务完成)才开始,烤面包(第四个任务)也不需要等待培根煎好(等待第三个任务完成)后才开始。(这是异步的方法)
放在编程中:
当一个方法被调用,调用者需要等待该方法执行完毕并返回才能继续执行,这个方法是同步方法。
当一个方法被调用立即返回,并获取一个线程执行该方法内部的业务,调用者不用等待该方法执行完毕并返回,而是继续执行后续代码,这个方法是异步方法。
如果方法里含有异步操作,那么整个方法都是异步的。
异步的实现
async/await 的使用
一个异步操作被抽象为Task
, 若有返回值则是Task<T>
async Task DoAsync()
{
}
async Task Caller()
{
await DoAsync();
}
async
是一个专门给编译器的提示,意思是该函数的实现可能会出现await
。
await
意为等待,需要等待await后面的函数运行完并且有返回结果后,才继续执行后续代码。异步方法使用await
来指定一个暂停点。
异步编程不一定涉及多线程,而是利用异步任务的等待和非阻塞特性来提高程序的并发性。
以官方文档给出的示例为例:
public async Task<int> GetUrlContentLengthAsync()
{
var client = new HttpClient();
Task<string> getStringTask = client.GetStringAsync("https://learn.microsoft.com/dotnet");
DoIndependentWork();
string contents = await getStringTask;
return contents.Length;
}
void DoIndependentWork()
{
Console.WriteLine("Working...");
}
GetUrlContentLengthAsync()
有async
修饰符,返回类型为Task<int>
。
contents = await getStringTask;
这一行代码,表示等待getStringTask
执行完毕,并且有返回结果后,才继续执行GetUrlContentLengthAsync()
内部的后续代码。(等待,指定的一个暂停点)
在getStringTask
完成之前,控件返回至GetUrlContentLengthAsync
的调用方。这样不会阻塞执行GetUrlContentLengthAsync
的线程,该线程可以被释放用来执行其他工作。
当getStringTask
完成时,控件将在此继续。
回到开头做早餐的例子:4,5 可以合并到一起形成一个Task
static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
var toast = await ToastBreadAsync(number);
一个厨师开始烤面包,并等待面包烤好。在等待过程中,当前的厨师(该线程)可以做其他事情。比如,回到调用方,去煎蛋。
以下是 https://learn.microsoft.com/zh-cn/dotnet/csharp/asynchronous-programming/ 给出的最终代码运行的一种结果
Pouring coffee
coffee is ready
Warming the egg pan...
putting 3 slices of bacon in the pan
cooking first side of bacon...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
cracking 2 eggs
cooking the eggs ...
flipping a slice of bacon
flipping a slice of bacon
flipping a slice of bacon
cooking the second side of bacon...
Remove toast from toaster
Putting butter on the toast
Putting jam on the toast
toast is ready
Put bacon on plate
Put eggs on plate
bacon is ready
eggs are ready
Pouring orange juice
oj is ready
Breakfast is ready!
可以看到Start toasting...
进程没有一直等待,在Remove toast from toaster
之前,回到调用方做了其他事情
关于async
标识
async
标识会告诉编译器这个方法里面可能会用到await
关键字来标识该方法是异步的,这样,编译器将会在状态机中编译此方法。该方法执行到await关键字时会处于挂起状态,直到该异步操作完成后才恢复继续执行后续操作。- 当将方法用
async
标识时且返回值为void
或者Task
或者Task<TReuslt>
,此时该方法会在当前线程中一直同步执行。 - 标记
async
,仅仅是异步的一个必要不充分条件
使用ConfigureAwait(false)
来避免死锁
public async Task<string> GetStringAsync()
{
// 在某个给定的线程上进入这个方法
// funcA()
await SomeAsyncMethod().ConfigureAwait(false);
// 可能在与开始时 funA() 不用的线程上执行
// funcB()
}
这允许代码继续执行在任何可用的线程上,确保不捕获同步上下文并防止潜在的死锁。
参考文档: