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

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


下文以個人對async/await的理解為基礎(chǔ)進行一些說明。


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

  1. 調(diào)用流阻塞:不同于線程阻塞,調(diào)用流阻塞只對函數(shù)過程起作用,調(diào)用流阻塞表示在一次函數(shù)調(diào)用中,執(zhí)行函數(shù)代碼的過程中發(fā)生的無法繼續(xù)往后執(zhí)行,需要在函數(shù)體中的某個語句停止的情形;
  1. 調(diào)用流阻塞點:調(diào)用流阻塞中,執(zhí)行流所停下來地方的那條語句;
  2. 調(diào)用流阻塞返回:不同于線程阻塞,調(diào)用流發(fā)生阻塞的時候,調(diào)用流會立即返回,在C#中,返回的對象可以是Task或者Task<T>
  3. 調(diào)用流阻塞異步完成跳轉(zhuǎn):當(dāng)調(diào)用流阻塞點處的異步操作完成后,調(diào)用流被強制跳轉(zhuǎn)回調(diào)用流阻塞點處執(zhí)行下一個語句的情形;
  4. async傳染:指的是根據(jù)C#的規(guī)定:若某個函數(shù)F的函數(shù)體中需要使用await關(guān)鍵字的函數(shù)必須以async標(biāo)記,進一步導(dǎo)致需要使用await調(diào)用F的那個函數(shù)F'也必須以async標(biāo)記的情況;
  5. Task對象的裝箱與拆箱:指Task<T>和T能夠相互轉(zhuǎn)換的情況。
  6. 異步調(diào)用:指以await作為修飾前綴進行方法調(diào)用的調(diào)用形式,異步調(diào)用時會發(fā)生調(diào)用流阻塞。
  7. 同步調(diào)用:指不以await作為修飾前綴進行方法調(diào)用的調(diào)用形式,同步調(diào)用時不會發(fā)生調(diào)用流阻塞。

2、async/await的使用場景

async/await用于異步操作。

在使用C#編寫GUI程序的時候,如果有比較耗時的操作(如圖片處理、數(shù)據(jù)壓縮等),我們一般新開一個線程把這些工作交給這個線程處理,而不放到主線程中進行操作,以免阻塞UI刷新,造成程序假死。

傳統(tǒng)的做法是直接使用C#的Thread類(也存在別的方式,參考這篇文章)進行操作。傳統(tǒng)的做法在復(fù)雜的應(yīng)用編寫中可能會出現(xiàn)回調(diào)地獄的問題,因此C#目前主要推薦使用async/await來進行異步操作。

async/await通過對方法進行修飾把C#中的方法分為同步方法和異步方法兩類,異步方法命名約定以Async結(jié)尾。但是需要注意的是,在調(diào)用異步方法的時候,并非一定是以異步方式來進行調(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");
        }
    }
}

在我的計算機上,執(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(),生成一個新的線程并執(zhí)行,同時立即返回一個Task對象;
  4. 由于調(diào)用Task.Run()時,是以await作為修飾的,因此是一個異步調(diào)用,上下文環(huán)境保存第4步中返回的Task對象,在此處發(fā)生調(diào)用流阻塞,而當(dāng)前的調(diào)用語句便是調(diào)用流阻塞點,于是發(fā)生調(diào)用流阻塞返回,執(zhí)行流回到AysncCall()的GetValueAsync()處,并執(zhí)行下一步;

第5步之后就不好分析了,因為此時已經(jīng)新建了一個線程用來執(zhí)行后臺線程,如果計算機速度夠快,那么由于新建的線程代碼中有一個Thread.Sleep(1000);,因此線程會被阻塞,于是主線程會趕在新建的線程恢復(fù)執(zhí)行之前打印End然后Console.ReadKey()。在這里我假設(shè)發(fā)生的是這個情況,然后進入下面的步驟。

  1. 新的線程恢復(fù)執(zhí)行,打印0 1 2 3 4 5,線程執(zhí)行結(jié)束,Task對象的IsCompleted變成true
  1. 此時執(zhí)行流(強制被)跳轉(zhuǎn)到調(diào)用流阻塞點,即從調(diào)用流阻塞點恢復(fù)執(zhí)行流,發(fā)生了調(diào)用流阻塞異步完成跳轉(zhuǎn),于是打印Task End;
  2. 程序執(zhí)行流結(jié)束;

仔細研究以上流程,可以發(fā)現(xiàn)async/await最重要的地方就是調(diào)用流阻塞點,這里的阻塞并不是阻塞的線程,而是阻塞的程序執(zhí)行流。整個過程就像是一個食客走進一間飯館點完菜,但是廚師說要等半個小時才做好(調(diào)用流阻塞),于是先給這個食客開了張單子(調(diào)用流阻塞點)讓他先去外面逛一圈(調(diào)用流阻塞返回),等時間到了會通知他然后他再拿這張票來吃飯(調(diào)用流阻塞異步完成跳轉(zhuǎn));整個過程中這個食客并沒有在飯館做下來等(線程阻塞),而是又去干了別的事情了。在這里,await就是用來指定調(diào)用流阻塞點的關(guān)鍵字,而async則是用來標(biāo)識某個方法可以被調(diào)用流阻塞的關(guān)鍵字。

4、假如不用await?

如果我們不使用await異步調(diào)用方法F的話,那么方法F將會被當(dāng)成同步方法調(diào)用,即發(fā)生同步調(diào)用,這個時候執(zhí)行流不會遇到調(diào)用流阻塞點,因此會直接往下執(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í)行流不會在Task.Run()這里停下返回,而是直接“路過”這里,執(zhí)行后面的語句,打印出Task End,然后和一般的程序一樣返回。當(dāng)然新的線程還是會被創(chuàng)建出來并執(zhí)行,但是這種情況下的程序就不會去等Task.Run()完成了。在我的計算機上輸出的結(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ī)定:若某個函數(shù)F的函數(shù)體中需要使用await關(guān)鍵字則該函數(shù)必須以async標(biāo)記,此時F成為異步方法,于是,這會導(dǎo)致這樣子的情況:需要使用await調(diào)用F的那個函數(shù)F'也必須以async標(biāo)記

這個現(xiàn)象我稱之為async傳染。

同時,C#又規(guī)定,Main函數(shù)不能夠是異步方法,這意味著至少在Main函數(shù)中是不能夠出現(xiàn)await異步調(diào)用的,進一步說明了任何的異步調(diào)用都是同步調(diào)用的子調(diào)用,而調(diào)用異步方法的那個方法我稱之為病源隔斷方法,因為在這里開始,不再會發(fā)生async傳染。

而在病源隔斷方法中,一般會在其他操作完成之后去等待異步操作完成:

// 病源隔斷方法
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>會自動和T完成裝箱拆箱操作。也就是說如果異步方法F返回Task<int>對象,那么當(dāng)異步方法完成的時候,它會自動變成int,整個過程由編譯器完成:

void async M()
{
    int r = await F();
}

// 異步方法
async Task<int> F() 
{
    await DoAsync();
    return 0;
}

這里說C#會自動完成Task<int>到T的裝箱和拆箱事實上是不嚴(yán)謹?shù)?,因為編譯器為我們隱藏了很多細節(jié),這里只是“看起來”像是有這么個過程,但實質(zhì)上并非如此。

事實上異步方法的返回值聲明聲明的只是調(diào)用阻塞返回值,并不是異步方法執(zhí)行完成后的真正返回值。造成這個事實的主要原因是存在調(diào)用阻塞返回真實方法返回兩個返回值,前一個是“臨時”的,而后一個是“執(zhí)行完成后”的,因此我們可以認為Task<int>對應(yīng)的是調(diào)用阻塞返回的返回值,而T這對應(yīng)的是真實方法返回的返回值。

我們可以把M進行改寫,事實上編譯器是為我們做了類似下面這樣子的工作:

void M()
{
    int r;
    Task<int> t = 獲取調(diào)用F()時的調(diào)用阻塞點的Task<int>對象;
    t.OnCompleted += () => {
        r = (int)t.Value;
    };
    t.Wait();
}

6、異步方法的定義約束

首先要明白的一點,就是async/await是不會主動創(chuàng)建線程(Task)的,創(chuàng)建線程的工作還是交給程序員來完成;async/await說白了就只是用來提供阻塞調(diào)用點的關(guān)鍵字而已。

因此,如果我們要定義一個異步方法,那么至少要保證:

  1. 在異步方法的調(diào)用中會出現(xiàn)新的線程(Task),無論調(diào)用層數(shù)有多深;
  2. 一個新線程(Task)應(yīng)該有且僅有一個阻塞調(diào)用點;
  3. 異步方法嵌套調(diào)用的時候, 每個嵌套調(diào)用的異步方法內(nèi)部至少要調(diào)用一個異步方法或者await一個返回值為Task的同步方法。

7、一個容易誤解的地方

考慮以下代碼:

async int M()
{
    return await F();
}

其中F()是一個異步方法,它返回的是Task<int>對象。

這段代碼事實上等價于:

async int M()
{
    int r = await F();
    return r;
}

注意和

async Task<int> M()
{
    return F();
}

區(qū)分。后面這段代碼是一個同步方法,它只會返回F()的真實返回值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容