異步(async、await)

同步與異步

同步:如果一個程序調用某個方法,等待其執行所有處理後才繼續執行,我們就稱這樣的方法是同步的,這是默認的形式
異步:異步的方法在處理完成之前就返回到調用方法,C#的async/await特性可以創建並使用異步方法。

async/await特性的組成

(1)調用方法:該方法調用異步方法,然後在異步方法(可能在相同的線程,也可能在不同的線程)執行其任務的時候繼續執行
(2)異步(async)方法:該方法異步執行其工作,然後立即返回到調用方法
(3)await表達式用於異步方法內部指明需要異步執行的任務一個異步方法可以包含任意多個await表達式,不過如果一個都不包含的話編譯器會發出警告

異步方法的定義規則

(1)方法頭中包含async方法修飾符它在返回類型之前。
(2)包含1個或多個await表達式,表示可以異步完成的任務
(3)方法的返回類型voidTaskTask<T>中的一種,Task和Task<T>的返回對象表示將在未來完成的工作,調用方法和異步方法可以繼續執行。
void:如果調用方法僅僅想執行異步方法,而不需要與它做任何進一步的交互時,異步方法可以返回void類型,這時即使異步方法中包含任何return語句,也不會返回任何東西。
Task:如果調用方法不需要從異步方法中返回某個值,但需要檢查異步方法的狀態,那麼異步方法可以返回一個Task類型的對象,這時,即使異步方法中出現了return語句,也不會返回任何東西

Task<T>:如果方法要從調用中獲取一個T類型的值,異步方法的返回類型就必須是Task<T>,調用方法將通過讀取Task的Result屬性來獲取這個T類型的值。

(4)任何返回Task<T>類型的異步方法其返回值必須爲T類型或可以隱式轉換爲T的類型
(5)異步方法的參數可以爲任意類型任意數量,但不能爲out、ref參數
(6)按照約定,異步方法的名稱應該以Async爲後綴
(7)除了方法以外,Lambda表達式和匿名方法也可以作爲異步對象。

異步方法結構

(1)第一個await表達式之前的部分:從方法開頭到第一個await表達式之間的所有代碼,這一部分應該只包含少量且無需長時間處理的代碼。
(2)await表達式:表示被異步執行的任務
(3)後續部分:在await表達式以後出現的方法中的其餘代碼,包括其執行環境,如所在線程信息、目前作用域的變量值,以及當await表達式完成後要重新執行所需的其他信息。

異步方法控制流

調用異步方法後,從第一個await表達式之前的代碼開始,正常(同步)執行直到遇見第一個await,這一區域實際上在第一個await表達式處結束,此時await任務還沒有完成(大多數情況下)。當await任務完成時,方法將繼續同步執行,如果還有其他await,就重複上述過程。

當達到await表達式時,異步方法將控制流返回到調用方法,如果方法的返回類型爲TaskTask<T>類型,將創建一個Task對象,表示需異步完成的任務和後續,然後將該Task返回到調用方法

目前有倆個控制流:異步方法內和調用方法內的,異步方法內的代碼完成以下工作,異步執行await表達式的空閒任務,當await表達式完成時,執行後續部分,後續部分本身也可能包含其他await表達式,這些表達式也將按照相同的方式處理,即異步執行await表達式,然後執行後續部分。當後續部分遇到return語句或到達方法末尾時,如果方法返回類型爲void,控制流將退出,如果方法返回類型爲Task,後續部分設置Task的屬性並退出,如果返回類型爲Task<T>,後續部分還將設置Task對象的Result屬性。

同時,調用方法中的代碼將繼續其進程,從異步方法獲取Task對象,當需要其實際值時,就引用Task對象的Result屬性,屆時,如果異步方法設置了該屬性,調用方法就能獲得該值並繼續,否則將暫停並等待屬性被設置,然後在執行

    class A
    {
        static public void DoRun()
        {
            var t = foo();
            Console.WriteLine(t.Result);
        }

        static private async Task<int> foo()
        {
            var t1 = Task.Run(() =>
              {
                  Thread.Sleep(100);
                  return 100;   //設置Task對象的Result屬性
              });


            var t2 = Task.Run(() =>
              {
                  Thread.Sleep(200);
                  return 200;   //設置Task對象的Result屬性
              });

            await Task.WhenAll(new List<Task<int>>() { t1, t2 }); //異步等待集合內的Task都完成,不會佔用主線程的時間

            return t1.Result+t2.Result;
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            A.DoRun();
        }
    }

await表達式

await表達式指定了一個異步執行的任務await關鍵字和一個空閒對象(稱爲任務)組成,這個任務可能是一個Task類型,也可能不是,默認情況下,這個任務在當前線程異步運行。

一個空閒對象即是一個awaitable類型的實例,awaitable類型是指包含GetAwaiter方法的類型,該方法沒有參數,返回一個稱爲awaiter類型的對象,awaiter類型包含以下成員
(1)bool IsCompleted {get;}
(2)void OnCompleted(Action);
它還包含以下成員之一:
(1)void GetResult();
(2)T GetResult(); (T爲任意類型)
實際上,並不需要構建自己的awaitable,相反,應該使用Task類,它是awaitable類型,作爲await表達式的任務,最簡單的方式是使用Task.Run方法來創建一個Task,關於Task.Run,有一點非常重要,即它是在不同的線程上運行你的方法
Task Run(Func<TReturn> func)   這個是Run函數衆多重載版本中的其中一個,參數爲一個委託類型,這個委託沒有參數,返回值類型爲TReturn,因此要將你的方法傳遞給Task.Run方法時,需要基於該方法創建一個委託。

取消一個異步操作

當我們想要終止異步執行時,可以通過類CancellationToken和類CancellationTokenSource來實現,擁有CancellationToken對象的任務需要定期檢查其令牌(token)狀態,如果CancellationToken對象的IsCancellationRequested屬性爲true,任務需停止其操作並返回CancellationToken是不可逆的,並且只能使用一次,也就是說,一旦IsCancellationRequested屬性被設置爲true,就不能更改了。

CancellationTokenSource對象創建可分配給不同任務的CancellationToken對象,任何持有CancellationTokenSource的對象都可以調用其Cancel方法,這會將CancellationToken的IsCancellationRequested屬性設置爲true

    class MyClass
    {
        public async Task RunAsync(CancellationToken ct)
        {
            if (ct.IsCancellationRequested)
            {
                Console.WriteLine(1111);
                return;
            }
            await Task.Run(() => CycleMethod(ct), ct);
        }

        void CycleMethod(CancellationToken ct)
        {
            Console.WriteLine("Starting CyclyMethod");
            const int max = 5;
            for (int i = 0; i < max; i++)
            {
                if (ct.IsCancellationRequested)
                {
                    Console.WriteLine("退出");
                    return;     //退出
                }
                Thread.Sleep(1000);
                Console.WriteLine("{0} of {1} iterations completed", i + 1, max);
            }
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            CancellationToken token = cts.Token;

            MyClass mc = new MyClass();
            Task t = mc.RunAsync(token);

            Thread.Sleep(3000);  //等待3秒
            cts.Cancel();        //取消異步

            t.Wait();    //等待這個Task完成執行過程
            Console.WriteLine("Was Cancelled:{0}", token.IsCancellationRequested);
        }
    }

輸出爲:

Starting CyclyMethod
1 of 5 iterations completed
2 of 5 iterations completed
3 of 5 iterations completed
退出
Was Cancelled:True

t.wait會等待這個Task結束後在繼續向下執行,如果沒有這行代碼,主程序會提前打印最後一樣代碼,然後退出,這樣就會少輸出一定內容,這和之前所說的一樣,假如異步方法返回的是Task<int>類型,那麼如果在主程序中如果使用到該返回的Result字段,則會等待這個值被賦值了之後再繼續向下走,否則主程序不會等待異步方法。

在調用方法中同步地等待任務(Wait)

調用方法可以調用任意多個異步方法並接收它們返回的Task對象,然後你的代碼會繼續執行其他任務,但在某個點上可能需要等待某個特殊Task對象完成,然後再繼續,爲此Task類提供了一個實例方法Wait,可以在Task對象上調用該方法。
Wait:用於等待單一Task對象
WaitAll(靜態方法):用於等待一組Task中所有任務都結束
WaitAny(靜態方法):用於等待一組Task中某一個任務結束

在異步方法中異步地等待任務(WhenAll、WhenAny)

有時在異步方法中,會希望用await表達式來等待Task,調用到await時異步方法會返回到調用方法,然後異步方法執行自己的流程,可以通過Task.WhenAllTask.WhenAny來實現等待一個或所有任務完成,然後接着執行後面語句。例如tasks是多個任務的集合,當調用到await Task.WhenAll(tasks); 則等待集合內所有任務都完成再繼續走下面的代碼,如果換成Task.WhenAny則集合內只要有一個任務完成則就走下面的代碼。

Task.Delay方法

Task.Delay方法創建一個Task對象,該對象將暫停其在線程中的處理,並在一定時間之後完成,與Thread.Sleep阻塞線程不同的是,Task.Delay不會阻塞線程,線程可以繼續處理其他工作。

Task.Yield

Task.Yield方法創建一個立即返回的awaitable,等待一個Yield可以讓異步方法在執行後續部分的同時返回到調用方法。可以將其理解成離開當前的消息隊列,回到隊列末尾,讓處理器有時間處理其他任務。

    static class DoStuff
    {
        public static async Task<int> FindSeriesSum(int i1)
        {
            int sum = 0;
            for(int i=0;i<i1;i++)
            {
                sum += i;
                if (i % 10 == 0)
                {
                    Console.WriteLine("前");
                    await Task.Yield();
                    Console.WriteLine("後");
                }
            }
            return sum;
        }
    }

    public class Program
    {
        static void Main(string[] args)
        {
            Task<int> value = DoStuff.FindSeriesSum(100);
            Console.WriteLine(111);
            Console.WriteLine(value.Result);
            Console.ReadLine();
        }
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章