關(guān)于C# async/await的一些說明

關(guān)于C# async/await的一些說明


下文以個(gè)人對async/await的理解為基礎(chǔ)進(jìn)行一些說明铅忿。


1派哲、自定義的幾個(gè)關(guān)鍵概念

  1. 調(diào)用流阻塞:不同于線程阻塞蹭睡,調(diào)用流阻塞只對函數(shù)過程起作用,調(diào)用流阻塞表示在一次函數(shù)調(diào)用中撩幽,執(zhí)行函數(shù)代碼的過程中發(fā)生的無法繼續(xù)往后執(zhí)行捐韩,需要在函數(shù)體中的某個(gè)語句停止的情形退唠;
  1. 調(diào)用流阻塞點(diǎn):調(diào)用流阻塞中,執(zhí)行流所停下來地方的那條語句荤胁;
  2. 調(diào)用流阻塞返回:不同于線程阻塞瞧预,調(diào)用流發(fā)生阻塞的時(shí)候,調(diào)用流會(huì)立即返回寨蹋,在C#中松蒜,返回的對象可以是Task或者Task<T>
  3. 調(diào)用流阻塞異步完成跳轉(zhuǎn):當(dāng)調(diào)用流阻塞點(diǎn)處的異步操作完成后已旧,調(diào)用流被強(qiáng)制跳轉(zhuǎn)回調(diào)用流阻塞點(diǎn)處執(zhí)行下一個(gè)語句的情形秸苗;
  4. 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)記的情況运褪;
  5. Task對象的裝箱與拆箱:指Task<T>和T能夠相互轉(zhuǎn)換的情況惊楼。
  6. 異步調(diào)用:指以await作為修飾前綴進(jìn)行方法調(diào)用的調(diào)用形式,異步調(diào)用時(shí)會(huì)發(fā)生調(diào)用流阻塞秸讹。
  7. 同步調(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í)行流程:

  1. Main()調(diào)用TestMain(),執(zhí)行流轉(zhuǎn)入TestMain();
  1. 打印Start
  2. 調(diào)用GetValueAsync()有鹿,執(zhí)行流轉(zhuǎn)入GetValueAsync()旭旭,注意此處是同步調(diào)用;
  3. 執(zhí)行Task.Run()葱跋,生成一個(gè)新的線程并執(zhí)行持寄,同時(shí)立即返回一個(gè)Task對象;
  4. 由于調(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)入下面的步驟湃交。

  1. 新的線程恢復(fù)執(zhí)行,打印0 1 2 3 4 5藤巢,線程執(zhí)行結(jié)束,Task對象的IsCompleted變成true息罗;
  1. 此時(shí)執(zhí)行流(強(qiáng)制被)跳轉(zhuǎn)到調(diào)用流阻塞點(diǎn)掂咒,即從調(diào)用流阻塞點(diǎn)恢復(fù)執(zhí)行流,發(fā)生了調(diào)用流阻塞異步完成跳轉(zhuǎn)迈喉,于是打印Task End绍刮;
  2. 程序執(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è)異步方法席舍,那么至少要保證:

  1. 在異步方法的調(diào)用中會(huì)出現(xiàn)新的線程(Task),無論調(diào)用層數(shù)有多深哮笆;
  2. 一個(gè)新線程(Task)應(yīng)該有且僅有一個(gè)阻塞調(diào)用點(diǎn)俺亮;
  3. 異步方法嵌套調(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í)返回值鲁冯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拷沸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子薯演,更是在濱河造成了極大的恐慌撞芍,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跨扮,死亡現(xiàn)場離奇詭異序无,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)衡创,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評(píng)論 3 398
  • 文/潘曉璐 我一進(jìn)店門帝嗡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人璃氢,你說我怎么就攤上這事哟玷。” “怎么了一也?”我有些...
    開封第一講書人閱讀 167,709評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵巢寡,是天一觀的道長。 經(jīng)常有香客問我椰苟,道長抑月,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評(píng)論 1 296
  • 正文 為了忘掉前任尊剔,我火速辦了婚禮爪幻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘须误。我一直安慰自己挨稿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評(píng)論 6 397
  • 文/花漫 我一把揭開白布京痢。 她就那樣靜靜地躺著奶甘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪祭椰。 梳的紋絲不亂的頭發(fā)上臭家,一...
    開封第一講書人閱讀 52,158評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音方淤,去河邊找鬼钉赁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛携茂,可吹牛的內(nèi)容都是我干的你踩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼带膜!你這毒婦竟也來了吩谦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤膝藕,失蹤者是張志新(化名)和其女友劉穎式廷,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體芭挽,經(jīng)...
    沈念sama閱讀 46,203評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡滑废,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了览绿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片策严。...
    茶點(diǎn)故事閱讀 40,427評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖饿敲,靈堂內(nèi)的尸體忽然破棺而出妻导,到底是詐尸還是另有隱情,我是刑警寧澤怀各,帶...
    沈念sama閱讀 36,122評(píng)論 5 349
  • 正文 年R本政府宣布倔韭,位于F島的核電站,受9級(jí)特大地震影響瓢对,放射性物質(zhì)發(fā)生泄漏寿酌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評(píng)論 3 333
  • 文/蒙蒙 一硕蛹、第九天 我趴在偏房一處隱蔽的房頂上張望醇疼。 院中可真熱鬧,春花似錦法焰、人聲如沸秧荆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評(píng)論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽乙濒。三九已至,卻和暖如春卵蛉,著一層夾襖步出監(jiān)牢的瞬間颁股,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評(píng)論 1 272
  • 我被黑心中介騙來泰國打工傻丝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留甘有,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,808評(píng)論 3 376
  • 正文 我出身青樓葡缰,卻偏偏與公主長得像亏掀,于是被迫代替她去往敵國和親允睹。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容