本文主要介紹了在 C# 中使用 Async 和 Await 關(guān)鍵字進(jìn)行異步編程的心得,是入門級(jí)的學(xué)習(xí)筆記咐低。
題解:關(guān)于「再次」這個(gè)說(shuō)法揽思,是因?yàn)榍皫状螌W(xué)習(xí)都失敗了,這要怪微軟在 MSDN 上給出的那張執(zhí)行順序圖见擦,實(shí)在是擰麻花钉汗。光是弄清楚所謂的「不阻塞 UI 線程,立即返回鲤屡,等到執(zhí)行結(jié)束后繼續(xù)后面的操作」這句話是什么含義儡湾,我就折騰了好久。
其實(shí) C# 5.0 推出的異步执俩,是個(gè)「人人用了都說(shuō)好」的事情,極大地簡(jiǎn)化了多線程的實(shí)現(xiàn)方式癌刽,所以應(yīng)該更接地氣才對(duì)的役首。只不過(guò)尝丐,眾多教程都是秉持科學(xué)嚴(yán)謹(jǐn)專業(yè)的態(tài)度,不能給初學(xué)者一個(gè)直觀的感受衡奥,讓初學(xué)者一下子透徹地理解——異步時(shí)爹袁,電腦究竟在干什么。
在「終于」學(xué)會(huì)異步之時(shí)矮固,激動(dòng)地想要實(shí)現(xiàn)化繁為簡(jiǎn)的偉大事業(yè)失息,遂撰此文。只講故事档址,不說(shuō)技術(shù)細(xì)節(jié)盹兢。
文章結(jié)構(gòu)
- 前言
- 0x00 異步是個(gè)什么東西
- 0x01 如何編寫異步代碼
- 0x02 異步代碼的執(zhí)行順序(★)
- 0x03 一些問(wèn)題的解答
- 0x04 本文沒(méi)有介紹的內(nèi)容
- 參考
0x00 異步是個(gè)什么東西
哦,異步(Asynchrony)就是「非同步」(A-Synchrony)
這里你要知道守伸,英語(yǔ)中 a(n) 作為前綴绎秒,是可以表示 not 或 without 的,所以「不同步性」尼摹、「沒(méi)有同步性」就是「異步性」了)见芹。
中文里,「同步」給人的感覺(jué)是「同時(shí)進(jìn)行的事情」蠢涝,例如「挖土和蓋樓兩件事情同步進(jìn)行中」玄呛,表示的是我們?cè)谕谕恋耐瑫r(shí)也在蓋大樓。然而在碼農(nóng)的世界里和二,「同步」的意思其實(shí)是「序貫執(zhí)行」徘铝,說(shuō)人話就是「一個(gè)接一個(gè)地有序執(zhí)行」。例如你寫了 5 件事情在任務(wù)清單上儿咱,你必須得做完第一件再做第二件庭砍,順序不可跳躍。
這帶來(lái)一個(gè)問(wèn)題:當(dāng)上一項(xiàng)任務(wù)沒(méi)有完成時(shí)混埠,下一項(xiàng)任務(wù)無(wú)法開始怠缸。那么當(dāng)你有 30 分鐘燒開水、10 分鐘洗茶杯钳宪、10 分鐘洗茶壺揭北、10分鐘找到茶葉這四件事情要完成時(shí),你無(wú)法先去把水燒上吏颖,等它燒開的同時(shí)搔体,洗杯子,洗壺半醉,找茶葉疚俱,只能傻等到水燒開了之后再去做剩下的三件事。如此一來(lái)缩多,總共可以在 30 分鐘內(nèi)完成的事情呆奕,拖拖拉拉到 60 分鐘才能搞定养晋,實(shí)乃人力物力財(cái)力的巨大浪費(fèi)。
為了讓電腦聰明起來(lái)梁钾,提高效率绳泉,人們發(fā)明了「異步」的概念。所以其實(shí)「異步」才真正意味著「同時(shí)進(jìn)行」的事情姆泻,可以理解為「你去挖土零酪,我來(lái)蓋樓」,如此我們「各干各的拇勃,分工相異四苇,同時(shí)進(jìn)行,完事兒后一起交差」潜秋。放到沏茶的例子里蛔琅,就是在燒開水的同時(shí),去洗杯子峻呛,洗壺罗售,找茶葉,總共 30 分鐘完成钩述。
放在計(jì)算機(jī)上來(lái)講是這樣:WinForm 程序啟動(dòng)時(shí)寨躁,只有一個(gè)線程,即 UI 線程牙勘。此時(shí)所有的工作都是由 UI 線程來(lái)處理的职恳,當(dāng)工作量較小時(shí),一瞬間即可完成方面,用戶不會(huì)覺(jué)得有任何異樣放钦;而當(dāng)假設(shè)完成一件巨型計(jì)算量的工作需要 30 分鐘時(shí),這個(gè)線程就會(huì)拼命地不停地去計(jì)算這個(gè)結(jié)果恭金,而無(wú)暇顧及用戶對(duì) UI 的操作操禀,導(dǎo)致 UI 卡死無(wú)響應(yīng)。這種情況是要極力避免的横腿,任何時(shí)候都應(yīng)當(dāng)以向用戶提供實(shí)時(shí)操作反饋為第一目標(biāo)颓屑,所以那些個(gè)極其耗費(fèi)計(jì)算資源的事情,應(yīng)該扔到后臺(tái)去做耿焊。
這聽起來(lái)像是「多線程」揪惦?的確,異步其實(shí)是多線程編程的一種實(shí)現(xiàn)方法罗侯。與傳統(tǒng)方法相比器腋,異步在代碼寫法、實(shí)現(xiàn)方式、管理復(fù)雜度和異常處理方面更加便捷而高效蒂培。并且再愈,異步代碼的寫法,「看上去就像同步的代碼一樣」护戳,簡(jiǎn)單而直接,因而被賦予了這樣一個(gè)地位極高的名字垂睬。當(dāng)然了媳荒,異步的本質(zhì)仍然逃不開多線程,無(wú)論是調(diào)用別人的異步方法驹饺,還是編寫自己的異步方法钳枕,都是要新開線程來(lái)完成工作的,單線程的異步赏壹,本質(zhì)上還是同步的鱼炒。不過(guò)好在,異步的引入蝌借,使得這一過(guò)程得到了極大的簡(jiǎn)化昔瞧。
0x01 如何編寫異步代碼
關(guān)于這個(gè),MSDN 的官方講解應(yīng)該是介紹最為完全的了(使用 Async 和 Await 的異步編程(C# 和 Visual Basic))菩佑。但遺憾的是自晰,MSDN 本身解耦工作做得并不到位,存在嚴(yán)重的用術(shù)語(yǔ)解釋術(shù)語(yǔ)的問(wèn)題稍坯,以及出神入化的行文邏輯酬荞,讓初學(xué)者越看越暈,所以我們要從一個(gè)更小的切入點(diǎn)開始說(shuō)起瞧哟。
先來(lái)建立一下對(duì)異步編程模型各個(gè)要素的認(rèn)識(shí)混巧。
- 異步方法的返回值類型有三種:void,Task 和 Task<T>勤揩。
- 使用 async 關(guān)鍵字修飾方法簽名咧党,表示該方法為異步方法。
- 在異步方法內(nèi)使用 await 關(guān)鍵字來(lái)等待一個(gè)「可等待」類型雄可,實(shí)現(xiàn)異步凿傅。
這樣說(shuō)來(lái)太抽象,用一個(gè)例子來(lái)說(shuō)明数苫。假設(shè)我們要實(shí)現(xiàn)這樣的功能:點(diǎn)擊一個(gè)按鈕聪舒,進(jìn)行一個(gè)計(jì)算量巨大的操作,要耗時(shí) 30 秒鐘虐急,計(jì)算結(jié)束后在窗口內(nèi)顯示計(jì)算結(jié)果箱残。代碼如下:
private void button1_Click(object sender, EventArgs e)
{
var result = doSomething();
label1.Text = result;
}
private string doSomething()
{
System.Threading.Thread.Sleep(30000);
return "result";
}
這里將當(dāng)前線程掛起 30 秒,來(lái)模擬耗時(shí) 30 秒的計(jì)算過(guò)程。很顯然被辑,運(yùn)行程序點(diǎn)擊按鈕后燎悍,UI 會(huì)在 30 秒內(nèi)毫無(wú)響應(yīng),全身心地投入到了復(fù)雜的計(jì)算過(guò)程中盼理。
接下來(lái)我們用異步編程的方法來(lái)改善這一問(wèn)題谈山。異步編程的核心思想是,執(zhí)行異步方法宏怔,當(dāng)遇到 await 關(guān)鍵字時(shí)奏路,控制權(quán)立即返回給調(diào)用者,同時(shí)等待 await 語(yǔ)句所指代的異步方法的結(jié)束臊诊,當(dāng)方法執(zhí)行完畢返回結(jié)果時(shí)鸽粉,接著執(zhí)行 await 語(yǔ)句后面的代碼。
放在這里就是抓艳,當(dāng)點(diǎn)擊按鈕時(shí)触机,我們要進(jìn)行巨型耗時(shí)計(jì)算,此時(shí)我們希望將控制權(quán)立刻返還給 UI玷或,使得 UI 可以相應(yīng)用戶的其他操作儡首,同時(shí)在后臺(tái)進(jìn)行計(jì)算工作,當(dāng)?shù)贸鲇?jì)算結(jié)果時(shí)庐椒,我們把它顯示在窗口上椒舵。
那么就按照如下方法改造之前的代碼。
// 給事件處理器添加 async 關(guān)鍵字
private async void button1_Click(object sender, EventArgs e)
{
// 給對(duì)計(jì)算方法的調(diào)用添加 await 關(guān)鍵字
var result = await doSomething();
label1.Text = result;
}
// 將返回值類型改為 Task<string>
private Task<string> doSomething()
{
// 將計(jì)算操作放到一個(gè) Task<string> 中去约谈,新開線程
var t = Task.Run(() =>
{
// 使用 lambda 表達(dá)式定義計(jì)算和返回工作
System.Threading.Thread.Sleep(30000);
return "result";
});
// 返回這個(gè) Task
return t;
}
現(xiàn)在再運(yùn)行一遍笔宿,可以發(fā)現(xiàn),點(diǎn)擊按鈕后計(jì)算開始運(yùn)行棱诱,但是 UI 仍然可以響應(yīng)用戶的操作泼橘,例如對(duì)窗口的移動(dòng)、縮放迈勋,和點(diǎn)擊其他控件等等炬灭,30 秒后,計(jì)算完成靡菇,窗口上的標(biāo)簽控件給出了結(jié)果「result」重归;
關(guān)于程序的運(yùn)行順序,先按下不表厦凤,下文詳談鼻吮。來(lái)說(shuō)說(shuō)這里幾處關(guān)鍵的代碼變動(dòng)。
-
添加 async 關(guān)鍵字
添加 async 關(guān)鍵字的目的在于较鼓,將方法明示為一個(gè)異步方法椎木,從而在其內(nèi)部的 await 單詞會(huì)被識(shí)別為一個(gè)關(guān)鍵字违柏,如果方法簽名中沒(méi)有 async 關(guān)鍵字的話,方法體中的 await 是作為標(biāo)識(shí)符來(lái)識(shí)別的香椎,也就是說(shuō)你可以定義一個(gè)名為 await 的變量漱竖,例如
string await = "hehe"
(不推薦這么做)。因而要使用 await 語(yǔ)句畜伐,必須在方法簽名中加入 async 關(guān)鍵詞馍惹。其實(shí)這對(duì)于編譯器來(lái)說(shuō)是多余的,但對(duì)于代碼的可讀性而言大有裨益烤礁。 -
在對(duì) doSomething 方法的調(diào)用前添加 await 關(guān)鍵字
await 是異步編程的靈魂讼积,用于等待一個(gè)「可等待」(awaitable)的對(duì)象返回值,同時(shí)向異步方法的調(diào)用者返回控制權(quán)脚仔。這里,我們使用 Task 對(duì)象來(lái)實(shí)現(xiàn)計(jì)算任務(wù)舆绎。
-
將計(jì)算任務(wù)的返回值更改為 Task<string>
這里鲤脏,如果不了解 Task 的話,需要去補(bǔ)補(bǔ)課吕朵。這里的含義是「返回值類型為字符串的任務(wù)」猎醇。Task 本身是可等待的對(duì)象,因而可以作為 await 關(guān)鍵字操作的要素努溃。這個(gè)方法是 await 要等待的任務(wù)硫嘶,它本身是不需要用 async 關(guān)鍵字來(lái)修飾的。
-
建立新線程完成具體工作
- 用
Task.Run
方法直接將t
定義為一個(gè)新的 Task梧税,并且立刻執(zhí)行沦疾。由于 Task 本身是利用線程池在后臺(tái)執(zhí)行的,所以這一步是實(shí)現(xiàn)異步編程多線程步驟的核心第队。當(dāng)我們撰寫自己的異步實(shí)現(xiàn)方法(注意不是異步方法)時(shí)要進(jìn)行多線程的操作哮塞,否則代碼始終還是同步(按順序)執(zhí)行的。 - 變量
t
作為返回值凳谦,必須與方法簽名相同忆畅,是Task<string>
類型的,但是在Task.Run
中并沒(méi)有體現(xiàn)尸执,而是在參數(shù)中的 lambda 表達(dá)式所體現(xiàn)的家凯,因?yàn)?lambda 表達(dá)式代碼塊中返回了一個(gè)字符串。這里如有不明的地方如失,需要去補(bǔ)充一下關(guān)于 lambda 表達(dá)式的知識(shí)绊诲。實(shí)際上,也可以顯示地將t
定義為Task<string>.Run
岖常。
- 用
-
返回變量
t
異步實(shí)現(xiàn)方法
doSomething
的返回值類型是Task<string>
驯镊,為什么在調(diào)用方法中由類型為string
的變量接收返回值?這是由異步編程模型和 Task 模型內(nèi)部的邏輯所決定的,更多深入的內(nèi)容請(qǐng)參見文末的參考文獻(xiàn)板惑,此處不做過(guò)多介紹橄镜。
如此,我們就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的異步編程冯乘,不僅包含了編寫異步方法洽胶,也包含了編寫異步實(shí)現(xiàn)方法。這可能是我個(gè)人的說(shuō)法:異步方法就是簽名中包含 async
關(guān)鍵字裆馒,在方法體中包含 await
關(guān)鍵字姊氓,用來(lái)執(zhí)行異步操作的方法;而異步實(shí)現(xiàn)方法就是喷好,返回值類型為可等待的翔横,由多線程來(lái)執(zhí)行具體任務(wù)的方法。
在 .NET 4.5 中梗搅,微軟提供了一批已經(jīng)預(yù)先編寫好的異步實(shí)現(xiàn)方法禾唁,例如 HttpClient 對(duì)象的 GetStringAsync 方法,其返回值是 Task<string>
類型无切,我們可以在使用中直接編寫如下代碼:
using System.Net.Http;
......
private async void button1_Click(object sender, EventArgs e)
{
var result = await new HttpClient().GetStringAsync("about:blank");
label1.Text = result;
}
這樣荡短,我們就可以十分方便地實(shí)現(xiàn)異步編程,無(wú)序大量的多線程處理哆键,就可以實(shí)現(xiàn)后臺(tái)工作和前臺(tái)響應(yīng)兩不誤掘托。
或者,可以編寫自己的異步實(shí)現(xiàn)方法籍嘹,用來(lái)實(shí)現(xiàn)異步調(diào)用闪盔,如同上文的例子一樣。
0x02 異步代碼的執(zhí)行順序
在 Visual Studio 2012 和 2013 版的 MSDN 上噩峦,關(guān)于這個(gè)問(wèn)題锭沟,微軟提供了一張圖,就是下面這個(gè)识补。
遺憾的是族淮,雖然圖上畫的東西完全正確帜羊,但對(duì)于初學(xué)者來(lái)說(shuō)怕轿,實(shí)在是太懵圈了。我自己在學(xué)習(xí)的時(shí)候涧团,反復(fù)閱讀也只能是有一個(gè)抽象的印象切油,不能建立直觀的了解蝙斜,不清楚這背后究竟是什么邏輯。更要命的是澎胡,這兩份文檔現(xiàn)在已經(jīng)歸檔孕荠,不再維護(hù)娩鹉,而新版的文檔里一張圖都沒(méi)有。許多引用這張圖來(lái)講解異步編程的博客也都沒(méi)能給出足夠容易的表達(dá)稚伍。所以這事兒只好我自己想明白之后來(lái)做了弯予。
先來(lái)設(shè)想一個(gè)場(chǎng)景:老板想吃薯?xiàng)l并聽音樂(lè),于是對(duì)三個(gè)員工說(shuō):「我要吃薯?xiàng)l个曙,我要聽音樂(lè)」锈嫩。可是三個(gè)人剛聽到「我要吃薯?xiàng)l」就立刻轉(zhuǎn)身離開垦搬,去計(jì)劃如何做薯?xiàng)l了呼寸。任務(wù)內(nèi)容包括:買土豆,可能需要 10 分鐘時(shí)間猴贰;準(zhǔn)備廚具 对雪,這個(gè)很快就搞定;土豆買回來(lái)削皮清洗切絲下鍋炸米绕;完后就可以送回給老板了慌植。直到這個(gè)過(guò)程結(jié)束,三個(gè)員工才會(huì)去關(guān)心老板想要聽音樂(lè)的事情义郑。
用程序來(lái)表示這個(gè)過(guò)程,代碼如下:
private void 老板()
{
老板_我要吃薯?xiàng)l();
老板_我要聽音樂(lè)();
}
private void 老板_我要吃薯?xiàng)l()
{
員工.執(zhí)行(買土豆);
員工.執(zhí)行(準(zhǔn)備廚具);
var 薯?xiàng)l = 員工.執(zhí)行(處理土豆炸薯?xiàng)l);
老板.吃(薯?xiàng)l);
}
private void 老板_我要聽音樂(lè)()
{
員工.執(zhí)行(打開留聲機(jī)播放唱片);
老板.聽(愛的禮贊);
}
這個(gè)過(guò)程的問(wèn)題在于丈钙,做薯?xiàng)l這個(gè)事情進(jìn)行了 30 分鐘非驮,老板除了干等著什么都干不了,員工們也不聽使喚雏赦,直到薯?xiàng)l炸出來(lái)了劫笙,才去解決聽音樂(lè)的問(wèn)題。
現(xiàn)在對(duì)這個(gè)過(guò)程進(jìn)行異步改造星岗,代碼如下:
private void 老板()
{
老板_我要吃薯?xiàng)l();
老板_我要聽音樂(lè)();
}
private async void 老板_我要吃薯?xiàng)l()
{
var 買土豆 = Task.Run(() =>
{
// 這是一個(gè)返回值類型為 Task<土豆> 的匿名方法
return 員工.執(zhí)行(買土豆);
});
員工.執(zhí)行(準(zhǔn)備廚具);
var 土豆 = await 買土豆;
var 薯?xiàng)l = 員工.執(zhí)行(處理土豆炸薯?xiàng)l);
老板.吃(薯?xiàng)l);
}
private void 老板_我要聽音樂(lè)()
{
員工.執(zhí)行(打開留聲機(jī)播放唱片);
老板.聽(愛的禮贊);
}
現(xiàn)在這個(gè)故事的劇情就變成了:老板說(shuō)「我要吃薯?xiàng)l」填大,于是三個(gè)員工去開始做薯?xiàng)l。三人覺(jué)得這個(gè)過(guò)程可以分開來(lái)做俏橘,第一個(gè)人去買土豆允华;第二個(gè)人在原地等著買土豆的人回來(lái),一起炸薯?xiàng)l寥掐,在買來(lái)之前靴寂,這個(gè)人先行準(zhǔn)備廚具;第三個(gè)人回去報(bào)告老板召耘,薯?xiàng)l正在制作請(qǐng)稍等百炬,還有沒(méi)有別的事情要做。老板說(shuō)「我要聽音樂(lè)」污它,于是第三個(gè)人立馬去放音樂(lè)給老板聽剖踊。如此一來(lái)庶弃,老板手下的員工還聽使喚,并且不必非要等到薯?xiàng)l做好才能聽到音樂(lè)了德澈。當(dāng)薯?xiàng)l做好的時(shí)候歇攻,員工把做好的薯?xiàng)l呈送給老板,老板來(lái)吃薯?xiàng)l圃验。
個(gè)人覺(jué)得這個(gè)例子直觀多了:
- 一個(gè)員工去買土豆:即新生成一個(gè) Task掉伏,利用線程池在后臺(tái)執(zhí)行。
- 一個(gè)員工等在原地:
await
關(guān)鍵字澳窑,等待這個(gè)買土豆 Task 的執(zhí)行結(jié)果斧散,當(dāng)土豆買回來(lái)了,也即 Task 執(zhí)行結(jié)束后摊聋,繼續(xù)后面炸薯?xiàng)l的工作鸡捐;在買來(lái)之前,也即程序遇到await
關(guān)鍵字之前麻裁,這個(gè)人先做準(zhǔn)備廚具的工作箍镜。薯?xiàng)l炸出來(lái)之后,交給老板煎源。 - 一個(gè)員工回去報(bào)告老板:
await
關(guān)鍵詞色迂,立刻向調(diào)用方返回控制權(quán),也即手销,在薯?xiàng)l還沒(méi)做好歇僧,甚至是土豆還沒(méi)買來(lái)的時(shí)候,就將程序的控制權(quán)交回給老板
锋拖,執(zhí)行老板的下一條語(yǔ)句诈悍,即老板_我要聽音樂(lè)();
。
值得一提的是兽埃,這個(gè)例子中侥钳,我們沒(méi)有單獨(dú)編寫異步實(shí)現(xiàn)方法,而是直接在異步方法內(nèi)部定義了一個(gè) Task柄错,并在后面 await 之舷夺。
老板能手下有多少員工?理論上鄙陡,一大群?jiǎn)T工即線程池冕房,由 CLR(Common Language Runtime,公共語(yǔ)言運(yùn)行時(shí)) 根據(jù)計(jì)算機(jī)性能進(jìn)行分配和管理趁矾,所以實(shí)際上可以同時(shí)執(zhí)行的異步方法比三個(gè)員工這個(gè)案例多得多耙册。
不知道這樣講述下來(lái),關(guān)于異步的執(zhí)行順序是否會(huì)更加清晰而具體毫捣。
0x03 一些問(wèn)題的解答
-
異步一定能提高效率嗎详拙?
不一定帝际。異步本質(zhì)上還是多線程,只是簡(jiǎn)化多線程的實(shí)現(xiàn)方式饶辙。至于使用多線程編程時(shí)能否提高程序執(zhí)行效率蹲诀,取決于 CPU 核心數(shù),計(jì)算任務(wù)的復(fù)雜度以及該項(xiàng)任務(wù)本身是否適合被切分為并行計(jì)算模塊弃揽。過(guò)于頻繁地將不適合并行計(jì)算的任務(wù)拆分成異步編程中去脯爪,反而會(huì)導(dǎo)致密集計(jì)算性能的下降,因?yàn)榇藭r(shí)線程池會(huì)疲于應(yīng)對(duì)大量的線程調(diào)度操作矿微。
-
有 async 一定要有 await 嗎痕慢?
不一定。在標(biāo)記為 async 的方法中涌矢,不必須出現(xiàn) await 關(guān)鍵字掖举,只是若沒(méi)有 await 關(guān)鍵字,這個(gè)方法不是真正意義上的異步方法娜庇,它會(huì)與普通方法一樣是同步執(zhí)行的塔次。編譯器不會(huì)報(bào)錯(cuò),但會(huì)給出提示名秀。
相反励负,若要使用 await 關(guān)鍵字,則必須在方法簽名中包含 async 關(guān)鍵字匕得。否則 await 將被當(dāng)做標(biāo)識(shí)符熄守,而不能被當(dāng)做一個(gè)關(guān)鍵字來(lái)處理。也就是說(shuō)耗跛,當(dāng)一個(gè)方法的簽名中不包含 async 關(guān)鍵字時(shí),你甚至可以在方法體中把 await 作為變量名攒发。但這種操作是極其不推薦的调塌,很容易造成誤導(dǎo)。
-
異步方法的名稱一定要以「Async」為結(jié)尾嗎惠猿?
不一定羔砾。這只是習(xí)慣問(wèn)題,就跟微軟推薦所有的自定義特性后面都以「Attributes」為結(jié)尾一樣偶妖,這不是必須的姜凄,只是如果大家都這樣做了,理解起來(lái)更加方便一些趾访。具體情況取決于不同場(chǎng)合下的規(guī)范要求态秧。
-
使用 Task 并且 Run 了之后就實(shí)現(xiàn)異步了嗎?
不是扼鞋,這只是進(jìn)行了一次多線程操作申鱼,后面的語(yǔ)句還是同步執(zhí)行的愤诱。直到遇見 await 關(guān)鍵字,隨著控制權(quán)的返回捐友,才真正能實(shí)現(xiàn)異步淫半。
-
異步是線程安全的嗎?
理論上是的匣砖,這也是為什么異步編程模型能夠極大地簡(jiǎn)化傳統(tǒng)多線程操作所帶來(lái)的各種問(wèn)題的一大原因科吭。盡管 await 所指的對(duì)象運(yùn)行在其他線程上,但其后的語(yǔ)句還是會(huì)在原始線程上被執(zhí)行猴鲫。更深層次地說(shuō)对人,后續(xù)的語(yǔ)句實(shí)際上是使用 Task 的 ContinueWith 方法來(lái)實(shí)現(xiàn)的。所以我們大可以放心的在異步方法中修改諸如 UI 元素等由主線程管理的資源变隔。
但是规伐,異步編程模型只是簡(jiǎn)化了這個(gè)過(guò)程,而不能替代我們解決具體的數(shù)據(jù)同步問(wèn)題匣缘。如果在 await 之后有對(duì)其他共享資源的訪問(wèn)猖闪,而在 await 獲取執(zhí)行結(jié)果之前,這些資源已經(jīng)被其他線程修改肌厨,那么 await 后續(xù)語(yǔ)句執(zhí)行時(shí)所面對(duì)的數(shù)據(jù)內(nèi)容將是不可預(yù)測(cè)的培慌。
-
異步一定是返回控制權(quán)與等待結(jié)果同時(shí)進(jìn)行的嗎?
第一時(shí)間返回控制權(quán)是一定的柑爸,而等待與否要看任務(wù)執(zhí)行的狀態(tài)吵护。當(dāng)程序遇到 await 關(guān)鍵字時(shí),如果 Task 所指代的對(duì)象以極快的速度完成表鳍,那么異步方法內(nèi)部就會(huì)以同步執(zhí)行的方式繼續(xù)向后執(zhí)行 await 語(yǔ)句后面的操作馅而,不會(huì)產(chǎn)生等待。只有當(dāng) Task 沒(méi)有執(zhí)行完畢時(shí)譬圣,才會(huì)進(jìn)行等待瓮恭。流程如下圖所示。
這里有個(gè)問(wèn)題厘熟,即 await 要求 Task 一定要有執(zhí)行結(jié)果屯蹦,如果只是聲明了一個(gè) Task,但是沒(méi)有運(yùn)行绳姨,await 是不會(huì)繼續(xù)向后進(jìn)行的登澜。雖然編譯器不會(huì)報(bào)錯(cuò),但是程序會(huì)永無(wú)休止地等下去飘庄。例如下面的代碼:
private asycn void doSthNoResponse()
{
var t = new Task(() => {});
await t; // 永無(wú)休止地等下去
}
新人更容易犯的是造成程序鎖死(Deadlock)的事故脑蠕,例如如下代碼:
private void doSthDeadlock()
{
var t = new Task<string>(() => { return String.Empty; });
label1.Text = t.Result; // 鎖死
}
當(dāng)然了,這屬于關(guān)于 Task 使用的問(wèn)題跪削,這里不做詳述了空郊,有興趣可以參考 Stephen Cleary 的博客文章《Don't Block on Async Code》份招。
-
異步的循環(huán)嵌套?
這曾經(jīng)是個(gè)困擾我的問(wèn)題狞甚,尤其是我在看了微軟給的異步方法執(zhí)行流程圖之后:一旦在某個(gè)節(jié)點(diǎn)使用了 async 關(guān)鍵字锁摔,那么它內(nèi)部一定要包含一個(gè)異步方法,而它本身又是一個(gè)異步方法哼审,于是乎就要一層一層又一層的都變成異步方法才行谐腰。
實(shí)際上不必,如同前文所述涩盾。但是 Jon Skeet 在《C# in Depth》中強(qiáng)調(diào)十气,如果有可能的話,要秉持著「將異步進(jìn)行到底」的精神春霍,一路異步下去砸西,這樣有助于保持程序的穩(wěn)健性。但愿我理解得是對(duì)的址儒,或者他只是想說(shuō)養(yǎng)成這樣的習(xí)慣芹枷,可以給軟件開發(fā)帶來(lái)更多的益處。
-
用多個(gè)放在一起的 await 等待多個(gè)任務(wù)莲趣?
不行鸳慈。每一個(gè) await 在放回控制權(quán)給調(diào)用者的同時(shí),都是阻塞執(zhí)行結(jié)果的喧伞,不能夠通過(guò)多個(gè)并列的 await 語(yǔ)句來(lái)同時(shí)等待多個(gè)結(jié)果走芋。例如如下代碼:
private async void doSth() { var t1 = new Task.Run(() => {......}); var t2 = new Task.Run(() => {......}); var t3 = new Task.Run(() => {......}); await t1; await t2; await t3; }
這段代碼的意思其實(shí)基本無(wú)異于
private async void doSth() { var t1 = new Task.Run(() => {......}); var t2 = new Task.Run(() => {......}); var t3 = new Task.Run(() => {......}); t1.Wait(); t2.Wait(); t3.Wait(); }
前者只是比后者多了控制權(quán)的返回罷了。因此潘鲫,即便 t2 和 t3 在 t1 之前運(yùn)行結(jié)束翁逞,程序也會(huì)一直等到 t1 運(yùn)行結(jié)束才會(huì)繼續(xù)。正確的做法是使用 Task 的 WhenAll 或者 WhenAny 方法處理執(zhí)行結(jié)束的后續(xù)事宜溉仑。
0x04 本文沒(méi)有介紹的內(nèi)容
篇幅和水平所限熄攘,以下這些內(nèi)容沒(méi)有涉及,或者所談很淺彼念。如有需要了解詳細(xì)內(nèi)容的,應(yīng)當(dāng)參閱更加專業(yè)的書籍浅萧、文檔和博客逐沙。
-
異常處理
本文所介紹的主體是基于任務(wù)的異步編程模式(TAP,Task-based Asynchronous Pattern)洼畅,因此異常處理也是與 Task 對(duì)象高度相關(guān)聯(lián)的吩案,這需要專門去了解 Task 相關(guān)的異常捕獲和處理方法,以及在異步編程下的處理方法帝簇。雖然我學(xué)習(xí)徘郭、閱讀過(guò)這部分內(nèi)容靠益,但是覺(jué)得體系極為龐雜,由于水平有限残揉,就暫時(shí)不寫上來(lái)了胧后。畢竟本文的主旨是幫助初學(xué)者快速建立對(duì)異步編程模型的認(rèn)識(shí)。
-
異步的實(shí)現(xiàn)和編譯原理
嗯抱环,沒(méi)有這部分是因?yàn)榭强欤覜](méi)太看懂……
-
很多細(xì)節(jié)
這篇文章總之是過(guò)于籠統(tǒng)了些,很多細(xì)節(jié)上需要注意的小問(wèn)題可能無(wú)暇涉及镇草。比如說(shuō)眶痰,
Task
以及Task<T>
是最為推薦的異步實(shí)現(xiàn)方法返回值類型,因?yàn)榭梢詫?duì)任務(wù)執(zhí)行的狀態(tài)和異常進(jìn)行合理的控制梯啤。而void
類型的異步方法則多用于事件處理器(Event Handler)上竖伯。
這本是我自己想要整理的學(xué)習(xí)筆記,怕幾個(gè)月之后自己又忘了因宇,所以寫得稍微啰嗦了些七婴,希望是讓小白也能輕松看懂的水平。希望能給有需求人士帶來(lái)一定的幫助羽嫡。
參考
- C# in Depth, 3rd edition.
- Microsoft Visual C# 2013 Step by Step
- Async and Await - Stephen Cleary
- Don't Block on Async Code - Stephen Cleary
- Asynchronous Programming - MSDN
- Task-based Asynchronous Pattern (TAP) - MSDN
- Asynchronous Programming with Async and Await (C# and Visual Basic) - MSDN
- C# Async - How does it work? - Tomas Petricek's answer - stack overflow
- await使用中的阻塞和并發(fā) - 樓上那個(gè)蜀黍
- C#基礎(chǔ)系列——異步編程初探:async和await - 懶得安分
- 全面解析C#中的異步編程 - 小白哥哥
- C#異步編程 - 方小白
- [C#] 談?wù)劗惒骄幊蘟sync await - Never本姥、C