關(guān)于C# async/await的一些說明
下文以個(gè)人對async/await的理解為基礎(chǔ)進(jìn)行一些說明铅忿。
1派哲、自定義的幾個(gè)關(guān)鍵概念
- 調(diào)用流阻塞:不同于線程阻塞蹭睡,調(diào)用流阻塞只對函數(shù)過程起作用,調(diào)用流阻塞表示在一次函數(shù)調(diào)用中撩幽,執(zhí)行函數(shù)代碼的過程中發(fā)生的無法繼續(xù)往后執(zhí)行捐韩,需要在函數(shù)體中的某個(gè)語句停止的情形退唠;
- 調(diào)用流阻塞點(diǎn):調(diào)用流阻塞中,執(zhí)行流所停下來地方的那條語句荤胁;
- 調(diào)用流阻塞返回:不同于線程阻塞瞧预,調(diào)用流發(fā)生阻塞的時(shí)候,調(diào)用流會(huì)立即返回寨蹋,在C#中松蒜,返回的對象可以是Task或者Task<T>;
- 調(diào)用流阻塞異步完成跳轉(zhuǎn):當(dāng)調(diào)用流阻塞點(diǎn)處的異步操作完成后已旧,調(diào)用流被強(qiáng)制跳轉(zhuǎn)回調(diào)用流阻塞點(diǎn)處執(zhí)行下一個(gè)語句的情形秸苗;
- async傳染:指的是根據(jù)C#的規(guī)定:若某個(gè)函數(shù)F的函數(shù)體中需要使用await關(guān)鍵字的函數(shù)必須以async標(biāo)記,進(jìn)一步導(dǎo)致需要使用await調(diào)用F的那個(gè)函數(shù)F'也必須以async標(biāo)記的情況运褪;
- Task對象的裝箱與拆箱:指Task<T>和T能夠相互轉(zhuǎn)換的情況惊楼。
- 異步調(diào)用:指以await作為修飾前綴進(jìn)行方法調(diào)用的調(diào)用形式,異步調(diào)用時(shí)會(huì)發(fā)生調(diào)用流阻塞秸讹。
- 同步調(diào)用:指不以await作為修飾前綴進(jìn)行方法調(diào)用的調(diào)用形式檀咙,同步調(diào)用時(shí)不會(huì)發(fā)生調(diào)用流阻塞。
2璃诀、async/await的使用場景
async/await用于異步操作弧可。
在使用C#編寫GUI程序的時(shí)候,如果有比較耗時(shí)的操作(如圖片處理劣欢、數(shù)據(jù)壓縮等)棕诵,我們一般新開一個(gè)線程把這些工作交給這個(gè)線程處理,而不放到主線程中進(jìn)行操作凿将,以免阻塞UI刷新校套,造成程序假死。
傳統(tǒng)的做法是直接使用C#的Thread類(也存在別的方式牧抵,參考這篇文章)進(jìn)行操作笛匙。傳統(tǒng)的做法在復(fù)雜的應(yīng)用編寫中可能會(huì)出現(xiàn)回調(diào)地獄的問題,因此C#目前主要推薦使用async/await來進(jìn)行異步操作犀变。
async/await通過對方法進(jìn)行修飾把C#中的方法分為同步方法和異步方法兩類妹孙,異步方法命名約定以Async結(jié)尾。但是需要注意的是弛作,在調(diào)用異步方法的時(shí)候涕蜂,并非一定是以異步方式來進(jìn)行調(diào)用,只有指定了以await為修飾前綴的方法調(diào)用才是異步調(diào)用映琳。
3机隙、async/await的調(diào)用過程
考慮以下C#程序:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
TestMain();
}
static void TestMain()
{
Console.Out.Write("Start\n");
GetValueAsync();
Console.Out.Write("End\n");
Console.ReadKey();
}
static async Task GetValueAsync()
{
await Task.Run(()=>
{
Thread.Sleep(1000);
for(int i = 0; i < 5; ++i)
{
Console.Out.WriteLine(String.Format("From task : {0}", i));
}
});
Console.Out.WriteLine("Task End");
}
}
}
在我的計(jì)算機(jī)上蜘拉,執(zhí)行該程序得到以下結(jié)果:
Start
End
From task : 0
From task : 1
From task : 2
From task : 3
From task : 4
Task End
下面來分析該程序的執(zhí)行流程:
- Main()調(diào)用TestMain(),執(zhí)行流轉(zhuǎn)入TestMain();
- 打印Start
- 調(diào)用GetValueAsync()有鹿,執(zhí)行流轉(zhuǎn)入GetValueAsync()旭旭,注意此處是同步調(diào)用;
- 執(zhí)行Task.Run()葱跋,生成一個(gè)新的線程并執(zhí)行持寄,同時(shí)立即返回一個(gè)Task對象;
- 由于調(diào)用Task.Run()時(shí)娱俺,是以await作為修飾的稍味,因此是一個(gè)異步調(diào)用,上下文環(huán)境保存第4步中返回的Task對象荠卷,在此處發(fā)生調(diào)用流阻塞模庐,而當(dāng)前的調(diào)用語句便是調(diào)用流阻塞點(diǎn),于是發(fā)生調(diào)用流阻塞返回油宜,執(zhí)行流回到AysncCall()的
GetValueAsync()
處掂碱,并執(zhí)行下一步;
第5步之后就不好分析了慎冤,因?yàn)榇藭r(shí)已經(jīng)新建了一個(gè)線程用來執(zhí)行后臺(tái)線程疼燥,如果計(jì)算機(jī)速度夠快,那么由于新建的線程代碼中有一個(gè)Thread.Sleep(1000);
蚁堤,因此線程會(huì)被阻塞醉者,于是主線程會(huì)趕在新建的線程恢復(fù)執(zhí)行之前打印End然后Console.ReadKey()
。在這里我假設(shè)發(fā)生的是這個(gè)情況披诗,然后進(jìn)入下面的步驟湃交。
- 新的線程恢復(fù)執(zhí)行,打印0 1 2 3 4 5藤巢,線程執(zhí)行結(jié)束,Task對象的IsCompleted變成true息罗;
- 此時(shí)執(zhí)行流(強(qiáng)制被)跳轉(zhuǎn)到調(diào)用流阻塞點(diǎn)掂咒,即從調(diào)用流阻塞點(diǎn)恢復(fù)執(zhí)行流,發(fā)生了調(diào)用流阻塞異步完成跳轉(zhuǎn)迈喉,于是打印
Task End
绍刮; - 程序執(zhí)行流結(jié)束;
仔細(xì)研究以上流程挨摸,可以發(fā)現(xiàn)async/await最重要的地方就是調(diào)用流阻塞點(diǎn)孩革,這里的阻塞并不是阻塞的線程,而是阻塞的程序執(zhí)行流得运。整個(gè)過程就像是一個(gè)食客走進(jìn)一間飯館點(diǎn)完菜膝蜈,但是廚師說要等半個(gè)小時(shí)才做好(調(diào)用流阻塞),于是先給這個(gè)食客開了張單子(調(diào)用流阻塞點(diǎn))讓他先去外面逛一圈(調(diào)用流阻塞返回),等時(shí)間到了會(huì)通知他然后他再拿這張票來吃飯(調(diào)用流阻塞異步完成跳轉(zhuǎn))捣郊;整個(gè)過程中這個(gè)食客并沒有在飯館做下來等(線程阻塞)颅筋,而是又去干了別的事情了。在這里推沸,await就是用來指定調(diào)用流阻塞點(diǎn)的關(guān)鍵字备绽,而async則是用來標(biāo)識(shí)某個(gè)方法可以被調(diào)用流阻塞的關(guān)鍵字。
4鬓催、假如不用await肺素?
如果我們不使用await異步調(diào)用方法F的話,那么方法F將會(huì)被當(dāng)成同步方法調(diào)用宇驾,即發(fā)生同步調(diào)用倍靡,這個(gè)時(shí)候執(zhí)行流不會(huì)遇到調(diào)用流阻塞點(diǎn),因此會(huì)直接往下執(zhí)行飞苇,考慮上面的代碼如果寫成:
static async Task GetValueAsync()
{
Task.Run(()=>
{
Thread.Sleep(1000);
for(int i = 0; i < 5; ++i)
{
Console.Out.WriteLine(String.Format("From task : {0}", i));
}
});
Console.Out.WriteLine("Task End");
}
那么執(zhí)行流不會(huì)在Task.Run()
這里停下返回菌瘫,而是直接“路過”這里,執(zhí)行后面的語句布卡,打印出Task End雨让,然后和一般的程序一樣返回。當(dāng)然新的線程還是會(huì)被創(chuàng)建出來并執(zhí)行忿等,但是這種情況下的程序就不會(huì)去等Task.Run()
完成了栖忠。在我的計(jì)算機(jī)上輸出的結(jié)果如下:
Start
Task End
End
From task : 0
From task : 1
From task : 2
From task : 3
From task : 4
5、async傳染與病源隔斷方法
根據(jù)C#的規(guī)定:若某個(gè)函數(shù)F的函數(shù)體中需要使用await關(guān)鍵字則該函數(shù)必須以async標(biāo)記贸街,此時(shí)F成為異步方法庵寞,于是,這會(huì)導(dǎo)致這樣子的情況:需要使用await調(diào)用F的那個(gè)函數(shù)F'也必須以async標(biāo)記薛匪。
這個(gè)現(xiàn)象我稱之為async傳染捐川。
同時(shí),C#又規(guī)定逸尖,Main函數(shù)不能夠是異步方法古沥,這意味著至少在Main函數(shù)中是不能夠出現(xiàn)await異步調(diào)用的,進(jìn)一步說明了任何的異步調(diào)用都是同步調(diào)用的子調(diào)用娇跟,而調(diào)用異步方法的那個(gè)方法我稱之為病源隔斷方法岩齿,因?yàn)樵谶@里開始,不再會(huì)發(fā)生async傳染苞俘。
而在病源隔斷方法中盹沈,一般會(huì)在其他操作完成之后去等待異步操作完成:
// 病源隔斷方法
void M()
{
var task = F();
DoSomething();
if(task.IsCompleted)
{
// 類似Thread的join()方法
task.Wait();
}
}
// 異步方法
async Task F()
{
await DoAsync();
}
5、如果異步方法要返回值吃谣?
在上面的例子中乞封,異步方法都是返回的Task做裙,表示沒有返回值。而如果要返回值的話歌亲,那么就簡單地把Task換成Task<T>就行了菇用,其中T是你的返回值的類型。
C#的Task<T>會(huì)自動(dòng)和T完成裝箱拆箱操作陷揪。也就是說如果異步方法F返回Task<int>對象惋鸥,那么當(dāng)異步方法完成的時(shí)候,它會(huì)自動(dòng)變成int悍缠,整個(gè)過程由編譯器完成:
void async M()
{
int r = await F();
}
// 異步方法
async Task<int> F()
{
await DoAsync();
return 0;
}
這里說C#會(huì)自動(dòng)完成Task<int>到T的裝箱和拆箱事實(shí)上是不嚴(yán)謹(jǐn)?shù)呢孕澹驗(yàn)榫幾g器為我們隱藏了很多細(xì)節(jié),這里只是“看起來”像是有這么個(gè)過程飞蚓,但實(shí)質(zhì)上并非如此滤港。
事實(shí)上異步方法的返回值聲明聲明的只是調(diào)用阻塞返回值,并不是異步方法執(zhí)行完成后的真正返回值趴拧。造成這個(gè)事實(shí)的主要原因是存在調(diào)用阻塞返回和真實(shí)方法返回兩個(gè)返回值溅漾,前一個(gè)是“臨時(shí)”的,而后一個(gè)是“執(zhí)行完成后”的著榴,因此我們可以認(rèn)為Task<int>對應(yīng)的是調(diào)用阻塞返回的返回值添履,而T這對應(yīng)的是真實(shí)方法返回的返回值。
我們可以把M進(jìn)行改寫脑又,事實(shí)上編譯器是為我們做了類似下面這樣子的工作:
void M()
{
int r;
Task<int> t = 獲取調(diào)用F()時(shí)的調(diào)用阻塞點(diǎn)的Task<int>對象;
t.OnCompleted += () => {
r = (int)t.Value;
};
t.Wait();
}
6暮胧、異步方法的定義約束
首先要明白的一點(diǎn),就是async/await是不會(huì)主動(dòng)創(chuàng)建線程(Task)的问麸,創(chuàng)建線程的工作還是交給程序員來完成往衷;async/await說白了就只是用來提供阻塞調(diào)用點(diǎn)的關(guān)鍵字而已。
因此严卖,如果我們要定義一個(gè)異步方法席舍,那么至少要保證:
- 在異步方法的調(diào)用中會(huì)出現(xiàn)新的線程(Task),無論調(diào)用層數(shù)有多深哮笆;
- 一個(gè)新線程(Task)應(yīng)該有且僅有一個(gè)阻塞調(diào)用點(diǎn)俺亮;
- 異步方法嵌套調(diào)用的時(shí)候, 每個(gè)嵌套調(diào)用的異步方法內(nèi)部至少要調(diào)用一個(gè)異步方法或者await一個(gè)返回值為Task的同步方法疟呐。
7、一個(gè)容易誤解的地方
考慮以下代碼:
async int M()
{
return await F();
}
其中F()是一個(gè)異步方法东且,它返回的是Task<int>對象启具。
這段代碼事實(shí)上等價(jià)于:
async int M()
{
int r = await F();
return r;
}
注意和
async Task<int> M()
{
return F();
}
區(qū)分。后面這段代碼是一個(gè)同步方法珊泳,它只會(huì)返回F()的真實(shí)返回值鲁冯。