C# 通過同步和異步實(shí)現(xiàn)優(yōu)化做早餐的時(shí)間
概述
一天之計(jì)在于晨,每天的早餐也是必不可少,但是很多人為了節(jié)約時(shí)間,都是簡單的吃點(diǎn)湊合一下或干脆不吃早餐,這對(duì)于個(gè)人身體和工作效率來說,無疑是不合理的,那么要如何做一頓早餐呢?如何能節(jié)約做早餐的時(shí)間呢?本文以一個(gè)簡單的小例子,簡述如何做一頓早餐及如何優(yōu)化做早餐的時(shí)間。僅供學(xué)習(xí)分享使用,如有不足之處,還請(qǐng)指正。
正常情況下,做早餐可以分為以下幾個(gè)步驟:
- 倒一杯咖啡。
- 加熱平底鍋,然后煎兩個(gè)雞蛋。
- 煎三片培根。
- 烤兩片面包。
- 在烤面包上加黃油和果醬。
- 倒一杯橙汁。
同步方式做早餐
根據(jù)以上步驟進(jìn)行編程,做一份早餐需要編寫程序如下:
/// <summary>
/// 同步做早餐
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnBreakfast_Click(object sender, EventArgs e)
{
this.txtInfo.Clear();
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
//1. 倒一杯咖啡。
string cup = PourCoffee();
PrintInfo("咖啡沖好了");
//2. 加熱平底鍋,然后煎兩個(gè)雞蛋。
string eggs = FryEggs(2);
PrintInfo("雞蛋煎好了");
//3. 煎三片培根。
string bacon = FryBacon(3);
PrintInfo("培根煎好了");
//4. 烤兩片面包。
string toast = ToastBread(2);
//5. 在烤面包上加黃油和果醬。
ApplyButter(toast);
ApplyJam(toast);
PrintInfo("面包烤好了");
//6. 倒一杯橙汁。
string oj = PourOJ();
PrintInfo("橙汁倒好了");
PrintInfo("早餐準(zhǔn)備完畢!");
watch.Stop();
TimeSpan time = watch.Elapsed;
PrintInfo(string.Format("總運(yùn)行時(shí)間為:{0}秒", time.TotalSeconds.ToString("0.00")));
}
/// <summary>
/// 倒一杯咖啡
/// </summary>
/// <returns></returns>
private string PourCoffee()
{
PrintInfo("正在沖咖啡...");
return "咖啡";
}
/// <summary>
/// 抹果醬
/// </summary>
/// <param name="toast"></param>
private void ApplyJam(string toast) =>
PrintInfo("往面包抹果醬");
/// <summary>
/// 抹黃油
/// </summary>
/// <param name="toast"></param>
private void ApplyButter(string toast) =>
PrintInfo("往面包抹黃油");
/// <summary>
/// 烤面包
/// </summary>
/// <param name="slices"></param>
/// <returns></returns>
private string ToastBread(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
PrintInfo("往烤箱里面放面包");
}
PrintInfo("開始烤...");
Task.Delay(3000).Wait();
PrintInfo("從烤箱取出面包");
return "烤面包";
}
/// <summary>
/// 煎培根
/// </summary>
/// <param name="slices"></param>
/// <returns></returns>
private string FryBacon(int slices)
{
PrintInfo($"放 {slices} 片培根在平底鍋");
PrintInfo("煎第一片培根...");
Task.Delay(3000).Wait();
for (int slice = 0; slice < slices; slice++)
{
PrintInfo("翻轉(zhuǎn)培根");
}
PrintInfo("煎第二片培根...");
Task.Delay(3000).Wait();
PrintInfo("把培根放盤子里");
return "煎培根";
}
/// <summary>
/// 煎雞蛋
/// </summary>
/// <param name="howMany"></param>
/// <returns></returns>
private string FryEggs(int howMany)
{
PrintInfo("加熱平底鍋...");
Task.Delay(3000).Wait();
PrintInfo($"磕開 {howMany} 個(gè)雞蛋");
PrintInfo("煎雞蛋 ...");
Task.Delay(3000).Wait();
PrintInfo("雞蛋放盤子里");
return "煎雞蛋";
}
/// <summary>
/// 倒橙汁
/// </summary>
/// <returns></returns>
private string PourOJ()
{
PrintInfo("倒一杯橙汁");
return "橙汁";
}
同步做早餐示例
通過運(yùn)行示例,發(fā)現(xiàn)采用同步方式進(jìn)行編程,做一份早餐,共計(jì)15秒鐘,且在此15秒鐘時(shí)間內(nèi),程序處于【卡住】狀態(tài),無法進(jìn)行其他操作。如下所示:

同步做早餐示意圖
同步方式做早餐,就是一個(gè)做完,再進(jìn)行下一個(gè),順序執(zhí)行,如下所示:

同步方式為何會(huì)【卡住】?
因?yàn)樵诔绦蜻M(jìn)程中,會(huì)有一個(gè)主線程,用于響應(yīng)用戶的操作,同步方式下,做早餐的和前端頁面同在主線程中,所以當(dāng)開始做早餐時(shí),就不能響應(yīng)其他的操作了。這就是【兩耳不聞窗外事,一心只讀圣賢書】的境界。但如果讓用戶長時(shí)間處于等待狀態(tài),會(huì)讓用戶體驗(yàn)很不友好。比如,劉玄德三顧茅廬,大雪紛飛之下,諸葛亮在草廬中午睡,劉關(guān)張?jiān)诖笱┲徐o等。試問有幾人會(huì)有玄德的耐心,何況程序也不是諸葛亮,用戶也沒有玄德的耐心!
異步方式做早餐
上述代碼演示了不正確的實(shí)踐:構(gòu)造同步代碼來執(zhí)行異步操作。 顧名思義,此代碼將阻止執(zhí)行這段代碼的線程執(zhí)行任何其他操作。 在任何任務(wù)進(jìn)行過程中,此代碼也不會(huì)被中斷。 就如同你將面包放進(jìn)烤面包機(jī)后盯著此烤面包機(jī)一樣。 你會(huì)無視任何跟你說話的人,直到面包彈出。如何做才能避免線程阻塞呢?答案就是異步。 await 關(guān)鍵字提供了一種非阻塞方式來啟動(dòng)任務(wù),然后在此任務(wù)完成時(shí)繼續(xù)執(zhí)行。
首先更新代碼,對(duì)于耗時(shí)的程序,采用異步方式做早餐,如下所示:
private async void btnBreakfastAsync_Click(object sender, EventArgs e)
{
this.txtInfo.Clear();
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
//1. 倒一杯咖啡。
string cup = PourCoffee();
PrintInfo("咖啡沖好了");
//2. 加熱平底鍋,然后煎兩個(gè)雞蛋。
//Task<string> eggs = FryEggsAsync(2);
string eggs =await FryEggsAsync(2);
PrintInfo("雞蛋煎好了");
//3. 煎三片培根。
string bacon =await FryBaconAsync(3);
PrintInfo("培根煎好了");
//4. 烤兩片面包。
string toast =await ToastBreadAsync(2);
//5. 在烤面包上加黃油和果醬。
ApplyButter(toast);
ApplyJam(toast);
PrintInfo("面包烤好了");
//6. 倒一杯橙汁。
string oj = PourOJ();
PrintInfo("橙汁倒好了");
PrintInfo("早餐準(zhǔn)備完畢!");
watch.Stop();
TimeSpan time = watch.Elapsed;
PrintInfo(string.Format("總運(yùn)行時(shí)間為:{0}秒", time.TotalSeconds.ToString("0.00")));
}
/// <summary>
/// 異步烤面包
/// </summary>
/// <param name="slices"></param>
/// <returns></returns>
private async Task<string> ToastBreadAsync(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
PrintInfo("往烤箱里面放面包");
}
PrintInfo("開始烤...");
await Task.Delay(3000);
PrintInfo("從烤箱取出面包");
return "烤面包";
}
/// <summary>
/// 異步煎培根
/// </summary>
/// <param name="slices"></param>
/// <returns></returns>
private async Task<string> FryBaconAsync(int slices)
{
PrintInfo($"放 {slices} 片培根在平底鍋");
PrintInfo("煎第一片培根...");
await Task.Delay(3000);
for (int slice = 0; slice < slices; slice++)
{
PrintInfo("翻轉(zhuǎn)培根");
}
PrintInfo("煎第二片培根...");
await Task.Delay(3000);
PrintInfo("把培根放盤子里");
return "煎培根";
}
/// <summary>
/// 異步煎雞蛋
/// </summary>
/// <param name="howMany"></param>
/// <returns></returns>
private async Task<string> FryEggsAsync(int howMany)
{
PrintInfo("加熱平底鍋...");
await Task.Delay(3000);
PrintInfo($"磕開 {howMany} 個(gè)雞蛋");
PrintInfo("煎雞蛋 ...");
await Task.Delay(3000);
PrintInfo("雞蛋放盤子里");
return "煎雞蛋";
}
注意:通過測(cè)試發(fā)現(xiàn),異步方式和同步方式的執(zhí)行時(shí)間一致,所以采用異步方式并不會(huì)縮短時(shí)間,但是程序已不再阻塞,可以同時(shí)響應(yīng)用戶的其他請(qǐng)求。
優(yōu)化異步做早餐
通過上述異步方式,雖然優(yōu)化了程序,不再阻塞,但是時(shí)間并沒有縮短,那么要如何優(yōu)化程序來縮短時(shí)間,以便早早的吃上可口的早餐呢?答案就是在開始一個(gè)任務(wù)后,在等待任務(wù)完成時(shí),可以繼續(xù)進(jìn)行準(zhǔn)備其他的任務(wù)。 你也幾乎將在同一時(shí)間完成所有工作。 你將吃到一頓熱氣騰騰的早餐。通過合并任務(wù)和調(diào)整任務(wù)的順序,將大大節(jié)約任務(wù)的完成時(shí)間,如下所示:
/// <summary>
/// 優(yōu)化異步做早餐
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void btnBreakfast2_Click(object sender, EventArgs e)
{
this.txtInfo.Clear();
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
//1. 倒一杯咖啡。
string cup = PourCoffee();
PrintInfo("咖啡沖好了");
//2. 加熱平底鍋,然后煎兩個(gè)雞蛋。
Task<string> eggsTask = FryEggsAsync(2);
//3. 煎三片培根。
Task<string> baconTask = FryBaconAsync(3);
//4.5合起來 烤面包,抹果醬,黃油
Task<string> toastTask = MakeToastWithButterAndJamAsync(2);
string eggs = await eggsTask;
PrintInfo("雞蛋煎好了");
string bacon = await baconTask;
PrintInfo("培根煎好了");
string toast = await toastTask;
PrintInfo("面包烤好了");
//6. 倒一杯橙汁。
string oj = PourOJ();
PrintInfo("橙汁倒好了");
PrintInfo("早餐準(zhǔn)備完畢!");
watch.Stop();
TimeSpan time = watch.Elapsed;
PrintInfo(string.Format("總運(yùn)行時(shí)間為:{0}秒", time.TotalSeconds.ToString("0.00")));
}
/// <summary>
/// 組合任務(wù)
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
private async Task<string> MakeToastWithButterAndJamAsync(int number)
{
var toast = await ToastBreadAsync(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
在本例中,合并了【烤面包+抹果醬+抹黃油】為一個(gè)任務(wù),這樣是烤面包的同時(shí),可以煎雞蛋,煎培根,三項(xiàng)耗時(shí)任務(wù)同時(shí)執(zhí)行。在三個(gè)任務(wù)都完成是,早餐也就做好了,示例如下所示:

通過以上優(yōu)化示例發(fā)現(xiàn),通過合并任務(wù)和調(diào)整順序,做一份早餐,需要6.06秒。
優(yōu)化異步早餐示意圖
優(yōu)化后的異步做早餐,由于一些任務(wù)并發(fā)運(yùn)行,因此節(jié)約了時(shí)間。示意圖如下所示:

異步異常
上述示例假定所有的任務(wù)都可以正常完成,那么如果某一個(gè)任務(wù)執(zhí)行過程中發(fā)生了異常,要如何捕獲呢?答案是:當(dāng)任務(wù)無法成功完成時(shí),它們將引發(fā)異常。 當(dāng)啟動(dòng)的任務(wù)為 awaited 時(shí),客戶端代碼可捕獲這些異常。
例如當(dāng)烤面包的時(shí)候,烤箱突然著火了,如何處理異常呢?代碼如下所示:
private async void btnBreakfastAsync3_Click(object sender, EventArgs e)
{
try
{
this.txtInfo.Clear();
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
//1. 倒一杯咖啡。
string cup = PourCoffee();
PrintInfo("咖啡沖好了");
//2. 加熱平底鍋,然后煎兩個(gè)雞蛋。
Task<string> eggsTask = FryEggsAsync(2);
//3. 煎三片培根。
Task<string> baconTask = FryBaconAsync(3);
//4.5合起來 烤面包,抹果醬,黃油
Task<string> toastTask = MakeToastWithButterAndJamAsyncEx(2);
string eggs = await eggsTask;
PrintInfo("雞蛋煎好了");
string bacon = await baconTask;
PrintInfo("培根煎好了");
string toast = await toastTask;
PrintInfo("面包烤好了");
//6. 倒一杯橙汁。
string oj = PourOJ();
PrintInfo("橙汁倒好了");
PrintInfo("早餐準(zhǔn)備完畢!");
watch.Stop();
TimeSpan time = watch.Elapsed;
PrintInfo(string.Format("總運(yùn)行時(shí)間為:{0}秒", time.TotalSeconds.ToString("0.00")));
}
catch (AggregateException ex) {
PrintInfo("線程內(nèi)部異常");
PrintInfo(ex.StackTrace);
}
catch (Exception ex)
{
PrintInfo("其他異常");
PrintInfo(ex.Message);
}
}
/// <summary>
/// 組合任務(wù)
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
private async Task<string> MakeToastWithButterAndJamAsyncEx(int number)
{
var toast = await ToastBreadAsyncEx(number);
ApplyButter(toast);
ApplyJam(toast);
return toast;
}
/// <summary>
/// 異步烤面包異常
/// </summary>
/// <param name="slices"></param>
/// <returns></returns>
private async Task<string> ToastBreadAsyncEx(int slices)
{
for (int slice = 0; slice < slices; slice++)
{
PrintInfo("往烤箱里面放面包");
}
PrintInfo("開始烤...");
await Task.Delay(2000);
PrintInfo("著火了! 面包糊了!");
int a = 1, b = 0;
int i = a / b;//制造一個(gè)異常
//throw new InvalidOperationException("烤箱著火了!");
await Task.Delay(1000);
PrintInfo("從烤箱取出面包");
return "烤面包";
}
異步任務(wù)異常示例

請(qǐng)注意,從烤面包機(jī)著火到發(fā)現(xiàn)異常,有相當(dāng)多的任務(wù)要完成。 當(dāng)異步運(yùn)行的任務(wù)引發(fā)異常時(shí),該任務(wù)出錯(cuò)。 Task 對(duì)象包含 Task.Exception 屬性中引發(fā)的異常。 出錯(cuò)的任務(wù)在等待時(shí)引發(fā)異常。
需要理解兩個(gè)重要機(jī)制:異常在出錯(cuò)的任務(wù)中的存儲(chǔ)方式,以及在代碼等待出錯(cuò)的任務(wù)時(shí)解包并重新引發(fā)異常的方式。
當(dāng)異步運(yùn)行的代碼引發(fā)異常時(shí),該異常存儲(chǔ)在 Task 中。 Task.Exception 屬性為 System.AggregateException,因?yàn)楫惒焦ぷ髌陂g可能會(huì)引發(fā)多個(gè)異常。 引發(fā)的任何異常都將添加到 AggregateException.InnerExceptions 集合中。 如果該 Exception 屬性為 NULL,則將創(chuàng)建一個(gè)新的 AggregateException 且引發(fā)的異常是該集合中的第一項(xiàng)。
對(duì)于出錯(cuò)的任務(wù),最常見的情況是 Exception 屬性只包含一個(gè)異常。 當(dāng)代碼 awaits 出錯(cuò)的任務(wù)時(shí),將重新引發(fā) AggregateException.InnerExceptions 集合中的第一個(gè)異常。 因此,此示例的輸出顯示 InvalidOperationException 而不是 AggregateException。 提取第一個(gè)內(nèi)部異常使得使用異步方法與使用其對(duì)應(yīng)的同步方法盡可能相似。 當(dāng)你的場(chǎng)景可能生成多個(gè)異常時(shí),可在代碼中檢查 Exception 屬性。
高效的等待
通過以上示例,需要等待很多任務(wù)完成,然后早餐才算做好,那么如何才能高效優(yōu)雅的等待呢?可以通過使用 Task 類的方法改進(jìn)上述代碼末尾的一系列 await 語句。其中一個(gè) API 是 WhenAll,它將返回一個(gè)其參數(shù)列表中的所有任務(wù)都已完成時(shí)才完成的 Task,如下所示:
private async void btnBreakfastAsync4_Click(object sender, EventArgs e)
{
this.txtInfo.Clear();
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
//1. 倒一杯咖啡。
string cup = PourCoffee();
PrintInfo("咖啡沖好了");
//2. 加熱平底鍋,然后煎兩個(gè)雞蛋。
Task<string> eggsTask = FryEggsAsync(2);
//3. 煎三片培根。
Task<string> baconTask = FryBaconAsync(3);
//4.5合起來 烤面包,抹果醬,黃油
Task<string> toastTask = MakeToastWithButterAndJamAsync(2);
//等待任務(wù)完成
await Task.WhenAll(eggsTask, baconTask, toastTask);
PrintInfo("雞蛋煎好了");
PrintInfo("培根煎好了");
PrintInfo("面包烤好了");
//6. 倒一杯橙汁。
string oj = PourOJ();
PrintInfo("橙汁倒好了");
PrintInfo("早餐準(zhǔn)備完畢!");
watch.Stop();
TimeSpan time = watch.Elapsed;
PrintInfo(string.Format("總運(yùn)行時(shí)間為:{0}秒", time.TotalSeconds.ToString("0.00")));
}
另一種選擇是使用 WhenAny,它將返回一個(gè)當(dāng)其參數(shù)完成時(shí)才完成的 Task<Task>。如下所示:
private async void btnBreakfastAsync5_Click(object sender, EventArgs e)
{
this.txtInfo.Clear();
Stopwatch watch = Stopwatch.StartNew();
watch.Start();
//1. 倒一杯咖啡。
string cup = PourCoffee();
PrintInfo("咖啡沖好了");
//2. 加熱平底鍋,然后煎兩個(gè)雞蛋。
Task<string> eggsTask = FryEggsAsync(2);
//3. 煎三片培根。
Task<string> baconTask = FryBaconAsync(3);
//4.5合起來 烤面包,抹果醬,黃油
Task<string> toastTask = MakeToastWithButterAndJamAsync(2);
//等待任務(wù)完成
var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
Task finishedTask = await Task.WhenAny(breakfastTasks);
if (finishedTask == eggsTask)
{
PrintInfo("雞蛋煎好了");
}
else if (finishedTask == baconTask)
{
PrintInfo("培根煎好了");
}
else if (finishedTask == toastTask)
{
PrintInfo("面包烤好了");
}
breakfastTasks.Remove(finishedTask);
}
//6. 倒一杯橙汁。
string oj = PourOJ();
PrintInfo("橙汁倒好了");
PrintInfo("早餐準(zhǔn)備完畢!");
watch.Stop();
TimeSpan time = watch.Elapsed;
PrintInfo(string.Format("總運(yùn)行時(shí)間為:{0}秒", time.TotalSeconds.ToString("0.00")));
}
這就是由同步到異步再到優(yōu)化異步任務(wù)的逐步過程
以上就是C# 通過同步和異步實(shí)現(xiàn)優(yōu)化做早餐的時(shí)間的詳細(xì)內(nèi)容,更多關(guān)于C# 同步 異步的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Unity實(shí)現(xiàn)ScrollView滑動(dòng)吸附功能
這篇文章主要為大家詳細(xì)介紹了Unity實(shí)現(xiàn)ScrollView滑動(dòng)吸附功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09
C#Process的OutputDataReceived事件不觸發(fā)問題及解決
這篇文章主要介紹了C#Process的OutputDataReceived事件不觸發(fā)問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
C# DataSet的內(nèi)容寫成XML時(shí)如何格式化字段數(shù)據(jù)
許多讀者經(jīng)常詢問一個(gè)問題,那就是在將DataSet的內(nèi)容寫成XML時(shí),如何格式化字段數(shù)據(jù)。最常見的需求,就是希望日期時(shí)間值與數(shù)值數(shù)據(jù)能夠以所需的格式呈現(xiàn)于XML中。2009-02-02
C#使用BitConverter與BitArray類進(jìn)行預(yù)定義基礎(chǔ)類型轉(zhuǎn)換
這篇文章介紹了C#使用BitConverter與BitArray類進(jìn)行預(yù)定義基礎(chǔ)類型轉(zhuǎn)換的方法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-05-05
C# 圖片與二進(jìn)制轉(zhuǎn)換的簡單實(shí)例
這篇文章介紹了C# 圖片與二進(jìn)制轉(zhuǎn)換的簡單實(shí)例,有需要的朋友可以參考一下2013-09-09

