任务异步编程模型 - Task asynchronous programming model
首先,什么是异步?
通过使用异步编程,您可以避免性能瓶颈并增强应用程序的整体响应能力。但是,用于编写异步应用程序的传统技术可能很复杂,这使得它们难以编写、调试和维护。
异步对于可能阻塞的活动至关重要,如果此类活动在同步进程中被阻塞,则整个应用程序必须等待。
当您使用异步方法时,应用程序会继续响应 UI。例如,您可以调整窗口大小或最小化窗口,或者如果您不想等待它完成,您可以关闭应用程序。
为什么要需要任务异步编程模型 ?
任务异步编程模型优于异步代码的抽象。就像往常一样,您将代码编写为一系列语句。您可以阅读该代码,就好像每个语句在下一个开始之前完成一样。
下面我们逐渐展开说明为什么需要异步编程模型和如何使用她。
我们先来看下面的代码示例
using System.Threading.Tasks;
using UnityEngine;
public class Testc : MonoBehaviour
{
float btime;
string passtime
{
get { return " - 过去了" + (Time.realtimeSinceStartup - btime).ToString("F1"); }
}
void Start()
{
btime = Time.realtimeSinceStartup;
int do1 = PourCoffee();
Debug.Log("咖啡好了." + passtime);
int eggs = FryEggs(2);
Debug.Log("鸡蛋好了" + passtime);
int bacon = FryBacon(3);
Debug.Log("培根好了" + passtime);
int toast = ToastBread(2);
ApplyButter(3);
ApplyJam(4);
Debug.Log("面包已经准备好了" + passtime);
int oj = PourOJ();
Debug.Log("橙汁已经准备好了" + passtime);
Debug.Log("早餐好了!" + passtime);
}
//倒入一杯橙汁。
private int PourOJ()
{
Debug.Log("倒入一杯橙汁。" + passtime);
Task.Delay(1000).Wait();
return 1;
}
private void ApplyJam(int toast)
{
Debug.Log("在吐司上涂果酱" + passtime);
Task.Delay(1000).Wait();
}
private void ApplyButter(int toast)
{
Debug.Log("在吐司上涂黄油" + passtime);
Task.Delay(1000).Wait();
}
private int ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("把一片面包放进烤面包机里" + passtime);
}
Debug.Log("开始烤~~~" + passtime);
Task.Delay(2000).Wait();
Debug.Log("从烤面包机中取出烤面包片" + passtime);
return 1;
}
private int FryBacon(int slices)
{
Debug.Log($"把 {slices} 片培根放锅里" + passtime);
Debug.Log("培根的第一面..." + passtime);
Task.Delay(1000).Wait();
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("翻动一片培根" + passtime);
}
Debug.Log("培根的另一面..." + passtime);
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("翻动一片培根" + passtime);
}
Task.Delay(1000).Wait();
Debug.Log("把培根放在盘子里" + passtime);
return 1;
}
private int FryEggs(int howMany)
{
Debug.Log("加热煎蛋锅。" + passtime);
Task.Delay(3000).Wait();
Debug.Log($"烹饪{howMany}个鸡蛋 ..." + passtime);
Task.Delay(1000).Wait();
Debug.Log("鸡蛋煎好了放在盘子里" + passtime);
return 1;
}
private int PourCoffee()
{
Debug.Log("倒咖啡" + passtime);
Task.Delay(1000).Wait();
return 1;
}
}
这段代码是类似于以下列表的说明来解释如何制作早餐:
- 倒一杯咖啡。
- 热锅,然后煎两个鸡蛋。
- 煎三片培根。
- 烤两片面包。
- 在吐司中加入黄油和果酱。
- 倒入一杯橙汁。
我们先来看下运行结果
程序在运行后卡顿了大概12秒,因为总和是每个单独任务的时间之和。
这是一个人(单线程)处理所有这些事情。在你热锅的时候,你是全神贯注的等待锅,不会去做任何事情,也不会去想别的事情。如果您有烹饪经验,您可以异步执行这些指令。你可以热锅的时候去烤面包等等。做早餐是个异步工作的好例子,但是不是并行的。对于并行算法,您需要多个厨师(或线程)。一个做鸡蛋,一个做培根,等等。每个人都将专注于那一项任务。每个厨师(或线程)都会被同步阻塞,等待培根准备好翻转,或者吐司爆开。
计算机不像人类那样解释这些指令。计算机将在每个语句上阻塞,直到工作完成,然后再转到下一个语句。这造成了令人不满意的早餐。在较早的任务完成之前,不会启动较晚的任务。制作早餐需要更长的时间,有些食物在上菜前会变冷。
如果想让计算机异步执行上述指令,就必须编写异步代码。
成功的现代应用程序需要异步代码。在没有语言支持的情况下,编写异步代码需要回调、完成事件或其他掩盖代码原始意图的方法。同步代码的优势在于其分步操作使其易于扫描和理解。传统的异步模型迫使您关注代码的异步性质,而不是代码的基本操作。
不要阻塞,而是等待
前面的代码展示了一个不好的做法:构造同步代码来执行异步操作。正如所写,此代码阻止执行它的线程执行任何其他工作。当任何任务正在进行时,它不会被中断。就好像你把面包放进去后盯着烤面包机。你会忽略任何人和你说话,直到吐司爆裂。
让我们从更新此代码开始,以便在任务运行时线程不会阻塞。使用await关键字提供了一种非阻塞方式来启动任务,然后在该任务完成时继续执行。
using System.Threading.Tasks;
using UnityEngine;
public class Testcwait : MonoBehaviour
{
float btime;
string passtime
{
get { return " - 过去了" + (Time.realtimeSinceStartup - btime).ToString("F1"); }
}
async void Start()
{
btime = Time.realtimeSinceStartup;
int do1 = await PourCoffee();
Debug.Log("咖啡好了." + passtime);
int eggs = await FryEggs(2);
Debug.Log("鸡蛋好了" + passtime);
int bacon = await FryBacon(3);
Debug.Log("培根好了" + passtime);
int toast = await ToastBread(2);
await ApplyButter(3);
await ApplyJam(4);
Debug.Log("面包已经准备好了" + passtime);
int oj = await PourOJ();
Debug.Log("橙汁已经准备好了" + passtime);
Debug.Log("早餐好了!" + passtime);
}
//倒入一杯橙汁。
private async Task<int> PourOJ()
{
Debug.Log("倒入一杯橙汁。" + passtime);
await Task.Delay(1000);
return 1;
}
private async Task ApplyJam(int toast)
{
Debug.Log("在吐司上涂果酱" + passtime);
await Task.Delay(1000);
}
private async Task ApplyButter(int toast)
{
Debug.Log("在吐司上涂黄油" + passtime);
await Task.Delay(1000);
}
private async Task<int> ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("把一片面包放进烤面包机里" + passtime);
}
Debug.Log("开始烤~~~" + passtime);
await Task.Delay(2000);
Debug.Log("从烤面包机中取出烤面包片" + passtime);
return 1;
}
private async Task<int> FryBacon(int slices)
{
Debug.Log($"把 {slices} 片培根放锅里" + passtime);
Debug.Log("培根的第一面..." + passtime);
await Task.Delay(1000);
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("翻动一片培根" + passtime);
}
Debug.Log("培根的另一面..." + passtime);
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("翻动一片培根" + passtime);
}
await Task.Delay(1000);
Debug.Log("把培根放在盘子里" + passtime);
return 1;
}
private async Task<int> FryEggs(int howMany)
{
Debug.Log("加热煎蛋锅。" + passtime);
await Task.Delay(3000);
Debug.Log($"烹饪{howMany}个鸡蛋 ..." + passtime);
await Task.Delay(1000);
Debug.Log("鸡蛋煎好了放在盘子里" + passtime);
return 1;
}
private async Task<int> PourCoffee()
{
Debug.Log("倒咖啡" + passtime);
await Task.Delay(1000);
return 1;
}
}
对比两次Log发现,红框里的时间,第一次只有一个时间因为是阻塞的,这次运行后时间没有阻塞,另外总运行时间与初始同步版本大致相同。该代码尚未利用异步编程的一些关键特性。
这份代码在你热锅的时候也不会去烤面包或者别的事情,你仍然在等待锅热,但是至少你会回应任何其他事情了。如果是在多个订单的餐厅中,你可以开始做饭同时开始做另一份了。
现在,在等待任何尚未完成的已启动任务时,处理早餐的线程不会被阻塞。GUI 应用程序仍然可以等待用户响应。
启动并发任务 - tasks concurrently
在许多情况下,您希望立即启动几个独立的任务。然后,当每个任务完成时,您可以继续其他准备好的工作。在早餐类比中,这就是您如何更快地完成早餐。您还可以几乎在同一时间完成所有工作。你会得到一份热腾腾的早餐。
并发任务使您能够编写更类似于您实际创建早餐的方式的代码。你会同时开始煮鸡蛋、培根和吐司。由于每个都需要采取行动,因此您会将注意力转移到该任务上,处理下一个行动,然后等待其他需要您注意的事情。
让我们再次修改上面的代码
async void Start()
{
btime = Time.realtimeSinceStartup;
int do1 = await PourCoffee();
Debug.Log("咖啡好了." + passtime);
//这里我们不wait鸡蛋了
Task<int> tegg = FryEggs(2);
//这里我们不wait培根了
Task<int> bacon = FryBacon(3);
int toast = await ToastBread(2);
await ApplyButter(3);
await ApplyJam(4);
Debug.Log("面包已经准备好了" + passtime);
int oj = await PourOJ();
Debug.Log("橙汁已经准备好了" + passtime);
int egg = await tegg;
Debug.Log("鸡蛋好了" + passtime);
int ba = await bacon;
Debug.Log("培根好了" + passtime);
Debug.Log("早餐好了!" + passtime);
}
我们修改是去掉了热锅煎鸡蛋和培根的加热的await,把await放到了末尾。
我们看下运行结果
我们发现总时间用了6秒,就是因为我们没有等待煎鸡蛋和煎培根的时间而是去做了其他事情。
看上图,相当于我在热锅煎鸡蛋,煎培根的时候,我不去等待了,而是同时也去烤面包了,并且等面包考好,接下来倒完了果汁我们才拿到了烤糊了的鸡蛋和培根(如果等待烤面包的时间够久)。
这就是并行思维和写法。
写道这里相信大家对任务异步编程模型就有一定的理解了吧。
异步异常 - Asynchronous exceptions
异步的函数如果出现了错误,那么如何来捕获呢,假设我们烤面包时候着火了
private async Task<int> ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("把一片面包放进烤面包机里" + passtime);
}
Debug.Log("开始烤~~~" + passtime);
await Task.Delay(2000);
throw new InvalidOperationException("面包机着火了");
Debug.Log("从烤面包机中取出烤面包片" + passtime);
return 1;
}
我们运行后会出现警告,制作早餐这个事情会被终止。这里要注意的是,这是故意的,因为一旦烤面包机着火,操作将无法正常进行。
另外如果要捕获异常,那么可以通过Task.Exception
//烤面包
Task<int> toast = ToastBread(2);
try
{
int itoast = await toast;
}
catch
{
Debug.LogError("出现错误了:" + toast.Exception.ToString());
}
await ApplyButter(3);
await ApplyJam(4);
Debug.Log("面包已经准备好了" + passtime);
高效等待任务 - Await tasks efficiently
- WhenAll等待
//这里我们不wait鸡蛋了
Task<int> tegg = FryEggs(2);
//这里我们不wait培根了
Task<int> bacon = FryBacon(3);
//烤面包
Task<int> toast = ToastBread(2);
await Task.WhenAll(tegg, bacon, toast);
Debug.Log("鸡蛋好了" + passtime);
Debug.Log("培根好了" + passtime);
Debug.Log("烤面包好了" + passtime);
它返回一个Task,当其参数列表中的所有任务都完成时,它就会完成
- WhenAny等待
//这里我们不wait鸡蛋了
Task<int> tegg = FryEggs(2);
//这里我们不wait培根了
Task<int> bacon = FryBacon(3);
//烤面包
Task<int> toast = ToastBread(2);
var breakfastTasks = new List<Task> { tegg, bacon, toast };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == tegg)
{
Debug.Log("鸡蛋好了" + passtime);
}
else if (finishedTask == bacon)
{
Debug.Log("培根好了" + passtime);
}
else if (finishedTask == toast)
{
Debug.Log("烤面包好了" + passtime);
}
breakfastTasks.Remove(finishedTask);
}
只要修改Start函数,增加数组并且遍历。它返回在Task其任何参数完成时完成的 。您可以等待返回的任务,知道它已经完成。以下代码显示了如何使用WhenAny等待第一个任务完成,然后处理其结果。处理完已完成任务的结果后,从传递给 的任务列表中删除该已完成任务WhenAny。
下面是最终代码的完整版
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using UnityEngine;
public class Testcwhen : MonoBehaviour
{
float btime;
string passtime
{
get { return " - 过去了" + (Time.realtimeSinceStartup - btime).ToString("F1"); }
}
async void Start()
{
btime = Time.realtimeSinceStartup;
int do1 = await PourCoffee();
Debug.Log("咖啡好了." + passtime);
//这里我们不wait鸡蛋了
Task<int> tegg = FryEggs(2);
//这里我们不wait培根了
Task<int> bacon = FryBacon(3);
//烤面包
Task<int> toast = ToastBread(2);
var breakfastTasks = new List<Task> { tegg, bacon, toast };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == tegg)
{
Debug.Log("鸡蛋好了" + passtime);
}
else if (finishedTask == bacon)
{
Debug.Log("培根好了" + passtime);
}
else if (finishedTask == toast)
{
Debug.Log("烤面包好了" + passtime);
}
breakfastTasks.Remove(finishedTask);
}
//await Task.WhenAll(tegg, bacon, toast);
//Debug.Log("鸡蛋好了" + passtime);
//Debug.Log("培根好了" + passtime);
//Debug.Log("烤面包好了" + passtime);
await ApplyButter(3);
await ApplyJam(4);
Debug.Log("面包已经准备好了" + passtime);
int oj = await PourOJ();
Debug.Log("橙汁已经准备好了" + passtime);
Debug.Log("早餐好了!" + passtime);
}
//倒入一杯橙汁。
private async Task<int> PourOJ()
{
Debug.Log("倒入一杯橙汁。" + passtime);
await Task.Delay(1000);
return 1;
}
private async Task ApplyJam(int toast)
{
Debug.Log("在吐司上涂果酱" + passtime);
await Task.Delay(1000);
}
private async Task ApplyButter(int toast)
{
Debug.Log("在吐司上涂黄油" + passtime);
await Task.Delay(1000);
}
private async Task<int> ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("把一片面包放进烤面包机里" + passtime);
}
Debug.Log("开始烤~~~" + passtime);
await Task.Delay(2000);
Debug.Log("从烤面包机中取出烤面包片" + passtime);
return 1;
}
private async Task<int> FryBacon(int slices)
{
Debug.Log($"把 {slices} 片培根放锅里" + passtime);
Debug.Log("培根的第一面..." + passtime);
await Task.Delay(1000);
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("翻动一片培根" + passtime);
}
Debug.Log("培根的另一面..." + passtime);
for (int slice = 0; slice < slices; slice++)
{
Debug.Log("翻动一片培根" + passtime);
}
await Task.Delay(1000);
Debug.Log("把培根放在盘子里" + passtime);
return 1;
}
private async Task<int> FryEggs(int howMany)
{
Debug.Log("加热煎蛋锅。" + passtime);
await Task.Delay(3000);
Debug.Log($"烹饪{howMany}个鸡蛋 ..." + passtime);
await Task.Delay(1000);
Debug.Log("鸡蛋煎好了放在盘子里" + passtime);
return 1;
}
private async Task<int> PourCoffee()
{
Debug.Log("倒咖啡" + passtime);
await Task.Delay(1000);
return 1;
}
}
这就是任务异步编程模型的用法了。我们应尽可能开始任务,不要阻止等待任务完成。
参考资料:https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/