原文地址 : 【A Simple Explanation of Async and Await in ASP.NET】
有時触机,我在日常工作中的技術(shù)分享迫使我去學(xué)習(xí)一些新的東西衷笋,有些甚至是以前從未接觸過的姑尺。上周就出現(xiàn)了這種情況,我的同事投票贊成下一次的技術(shù)分享主題為.NET 中的 Async 和 Await 異步編程
纹蝴。這兩個關(guān)鍵字使得異步編程在 .NET 4.5 之后變得更簡單了血柳。
老實說辙纬,直到上一周,對于異步編程我知之甚少。但在我做了一些研究并創(chuàng)建了我自己的簡單項目之后囚聚,我開始理解了為什么 Stephen Cleary 會說 :“異步編程將從根本上改變大多數(shù)代碼的編寫方式”靖榕。
本著不關(guān)心我的結(jié)巴,只要我在學(xué)習(xí)(參見),我決定直接學(xué)習(xí) async
, await
和 異步編程顽铸。 但是我遇到一個問題:我發(fā)現(xiàn)很少有資源能夠用簡單的術(shù)語向我解釋異步編程的概念茁计。
我試圖用這篇文章來彌補這個空缺。
我并不打算去詳細介紹 .NET 中關(guān)于異步編程的技術(shù)實現(xiàn)細節(jié)谓松,相反星压,我會專注于將這些不透明的概念分解成簡單的概念,使得它更好的被理解毒返。這幫助我更好的理解當我使用這些關(guān)鍵字時我正在做什么租幕,同時,希望它也能幫助到你拧簸。開始吧劲绪!
什么是異步編程?
異步編程是在編寫代碼盆赤,這些代碼允許在沒有“阻塞”的情況下同時發(fā)生幾件事情贾富,或者等待其它的事情完成。它不同于同步編程牺六,在同步編程中颤枪,所有的事情都按照它所寫的順序發(fā)生(如果你為一個活人編寫代碼,那么可能是同步代碼)淑际。
讓我們來看一個 C# 中同步編程的方法:
public string GetNameAndContent()
{
var name = GetLongRunningName(); //調(diào)用另一個webservice畏纲,需要 1 分鐘。
var content = GetContent(); //需要 30 秒
return name + ": " + content;
}
每一次調(diào)用這個方法春缕,調(diào)用者必須等待一分鐘才能恢復(fù)處理盗胀。這一分鐘的時間被浪費了,它可以用來做其他的事情锄贼。
而 .NET 的異步編程票灰,我們可以像這樣改變這個方法:
public async Task<string> GetNameAndContent()
{
var nameTask = GetLongRunningName(); //這個方法是異步的
var content = GetContent(); //這個方法是同步的
var name = await nameTask;
return name + ": " + content;
}
我們改變了這個方法的三個地方:
- 1.我們把方法改為了異步
async
。它會告訴編譯器這個方法可以異步執(zhí)行宅荤。 - 2.我們使用
await
關(guān)鍵字修飾 nameTask 變量屑迂,它告訴編譯器我們最終需要GetLongRunningName()
方法的結(jié)果,但是我們不需要阻塞這個調(diào)用冯键。 - 3.我們將方法的返回類型改為
Task<string>
惹盼。它通知調(diào)用者返回的最終類型為字符串,但不是立即獲得琼了,我們不可以做任何其他的事情直到GetLongRunningName()
方法調(diào)用結(jié)束逻锐。
但是即使是這樣夫晌,它任然很模糊。當 “等待” GetLongRunningName()
完成時昧诱,我們實際在做什么晓淀?
這很難用簡單的術(shù)語來表達,但我還是會嘗試的盏档。本質(zhì)上凶掰,系統(tǒng)希望執(zhí)行GetLongRunningName()
,因為它首先被調(diào)用蜈亩,但它是一個異步任務(wù)懦窘,所以我們需要等待它,控制器被拋出去執(zhí)行GetContent()
稚配,這意味著我們現(xiàn)在有兩種方法在同時運行畅涂。這不是轉(zhuǎn)移到另一個線程,使用 async
和 await
不會導(dǎo)致線程的創(chuàng)建道川。
(如果您想要更深入地解釋在異步調(diào)用過程中發(fā)生了什么午衰,參見MSDN。這是我能找到的最好的例子冒萄,有一個圖臊岸。)
等待多個調(diào)用
讓我來看另一個簡單是例子。有一個叫 ContentManagement 的類尊流,它既包含了同步方法也包含了異步方法:
public class ContentManagement
{
public string GetContent()
{
Thread.Sleep(2000);
return "content";
}
public int GetCount()
{
Thread.Sleep(5000);
return 4;
}
public string GetName()
{
Thread.Sleep(3000);
return "Matthew";
}
public async Task<string> GetContentAsync()
{
await Task.Delay(2000);
return "content";
}
public async Task<int> GetCountAsync()
{
await Task.Delay(5000);
return 4;
}
public async Task<string> GetNameAsync()
{
await Task.Delay(3000);
return "Matthew";
}
}
ContentManagement 是一個簡單的類帅戒,它模擬了一些需要長時間運行的執(zhí)行方法。注意崖技,其中有三個方法被標注了 async
逻住,并且(通過約定)將Async
這個關(guān)鍵字添加到方法名中。我們將在下面的潛在問題部分中解釋為什么我們需要同步和異步方法迎献。
現(xiàn)在鄙信,然給我們寫一個如下的 MVC 控制器:
public class HomeController : Controller
{
[HttpGet]
public ActionResult Index()
{
Stopwatch watch = new Stopwatch();
watch.Start();
ContentManagement service = new ContentManagement();
var content = service.GetContent();
var count = service.GetCount();
var name = service.GetName();
watch.Stop();
ViewBag.WatchMilliseconds = watch.ElapsedMilliseconds;
return View();
}
[HttpGet]
public async Task<ActionResult> IndexAsync()
{
Stopwatch watch = new Stopwatch();
watch.Start();
ContentManagement service = new ContentManagement();
var contentTask = service.GetContentAsync();
var countTask = service.GetCountAsync();
var nameTask = service.GetNameAsync();
var content = await contentTask;
var count = await countTask;
var name = await nameTask;
watch.Stop();
ViewBag.WatchMilliseconds = watch.ElapsedMilliseconds;
return View("Index");
}
}
(順便說一下,Stopwatch類在嘗試記錄執(zhí)行時間時非常有用忿晕。)
注意這兩個動作的作用。在這兩種情況下银受,操作都調(diào)用 ContentManagement 服務(wù)中的方法践盼,但在其中一個操作中,它們是異步執(zhí)行的宾巍。
在我們的 Index
視圖中咕幻,我們有一個顯示 WatchMilliseconds
值的輸出。讓我們來看看Index
中結(jié)果是什么:
直觀地說,這是有道理的顶霞。我們稱為三種方法;他們分別花了 2 秒肄程,5 秒和 3 秒來執(zhí)行;所以總執(zhí)行時間應(yīng)該是 2 + 5 + 3 = 10 秒锣吼。
現(xiàn)在看看如果我們調(diào)用 IndexAsync
操作會發(fā)生什么:
我們從哪里能得到這個數(shù)字? 這三個任務(wù)中的最長任務(wù)需要的時間為5秒蓝厌。 通過設(shè)計這個來使用async
玄叠,我們能減少總花費時長的一半!通過編寫一些額外的代碼就可以獲得很好的加速效果!
返回類型
如你所見拓提,我們?yōu)槭裁匆帉懏惒骄幊檀a读恃,現(xiàn)在看來至少對我來說是有意義的。但是代态,到底我們在 IndexAsync 操作中使用的Task<ActionResult>
是什么東西寺惫?
含有async
關(guān)鍵字的方法中有三種返回類型可以選擇:
- Task:這個類表示一個異步操作,并且可以被等待蹦疑;
- Task<T>:這個類表示一個有返回值的異步操作西雀,并且可以被等待;
- void:如果一個異步方法返回void歉摧,它不能被等待艇肴。這實際上把方法變成了“fire and forget(閱后即焚)”方法,這樣的情況很少出現(xiàn)判莉。進一步豆挽,返回void的異步方法的錯誤處理有點不同,比如shown by Stephen Cleary券盅。沒有理由使用void作為異步調(diào)用的返回類型帮哈,除非完全不關(guān)心調(diào)用是否實際完成。
簡而言之锰镀,幾乎所有的異步方法都會使用 Task 或 Task<T> 作為它們的返回類型娘侍。Task 類表示異步操作本身,而不是action的結(jié)果泳炉。在一個 Task 中調(diào)用 await
意味著我們需要等待這個 Task 執(zhí)行完成憾筏,而在 Task<T> 的情況下,需要檢索任務(wù)返回的值花鹅。
潛在問題
讓我們注意一些事情:大多數(shù)應(yīng)用程序在實現(xiàn)異步編程時可能不會看到這樣的戲劇性的改進(比如50%加速!)氧腰,同樣,我們不會對單個的方法進行壓力測試刨肃,只是同時執(zhí)行它們古拴。事實上,如果我們不正確地設(shè)計我們的異步方法真友,實際上可能會損害整體性能黄痪。
當我們將一個方法標記為 async
時,編譯器在后臺生成一個狀態(tài)機盔然;這是額外的代碼桅打。如果我們編寫好的是嗜、穩(wěn)定的異步代碼,創(chuàng)建這種額外結(jié)構(gòu)所需的時間不會對我們造成任何影響挺尾,因為運行異步的好處超過了構(gòu)建狀態(tài)機的成本鹅搪。然而,如果我們的 async
方法沒有 await
潦嘶,方法將會被同步運行涩嚣,我們將花費額外的時間來創(chuàng)建我們沒有使用的狀態(tài)機。
還有一個潛在的問題需要注意掂僵。我們不能從同步方法調(diào)用異步方法航厚。因為在所有情況下, async
和 await
應(yīng)該是一起的锰蓬,我們需要在所有的方法上都有異步幔睬。這就是為什么我們在前面的 ContentManagement 類中需要單獨的 async
方法。最終芹扭,這導(dǎo)致了更多的代碼麻顶,這意味著更多的東西理論上可以中斷。然而舱卡,由于對我們想要完成的事情有了良好的設(shè)計和堅實的理解辅肾,擁有額外的代碼也會帶來更大的性能,所以在我看來轮锥,這是一個公平的交易矫钓。
摘要
在實現(xiàn)異步的過程中付出一點額外的努力,對于提高我們的應(yīng)用程序的性能和響應(yīng)能力有很長的一段路要走舍杜。.NET使 async
和await
關(guān)鍵字變得容易新娜,我們可以簡單、簡潔地既绩、快速地實現(xiàn)異步設(shè)計概龄。
我在這個練習(xí)之前完全不懂異步編程;現(xiàn)在我至少可以說我不是一無所知饲握。希望你也一樣私杜!
如果你想學(xué)更多的內(nèi)容,可以看一下 Alex Davies 寫的 《Async in C# 5.0》 ,我在試圖理解MVC web應(yīng)用程序中異步/等待的情況時救欧,我讀過歪今,它幫了我很多。
我使用 Visual Studio 2015 和 ASP.NET MVC(包括我們之前使用的ContentManagement類和HomeController) 創(chuàng)建了一個簡單項目颜矿。我把它放到了Github,你可以check out下來進行練習(xí)嫉晶。