前言

先前我們介紹了 API 的基本用法,現在我們來介紹非同步的方法。

本文

首先還是要先提醒一下,對於非同步程式設計有很多眉眉角角,這邊只能簡單說明,所以若想更深入了解還是推薦大家尋求專業知識來源。

首推:

一、同步與非同步。

1、同步。

我們先來認識一下甚麼叫做同步的程式設計。
其實你已經寫過了。

拿之前寫過的 UpdataAccount Service 來做說明,我們很明確會知道這段程式會由上到下一步一步接續執行,圖示意就是以下。

(圖片取自.NET 本事-非同步程式設計)

而這種同步程式設計有很明顯的問題。

  • a、當有某項處理程序需要長時間的處理時,使用者與程式的互動體驗差,Ex:按下按鈕之後需要等到程式回應才會有其他動作。
  • b、當某個應用程式進入無窮迴圈導致其他應用程式暫停時,整個OS會像當機一樣,此時只能叫出工作管理員強制關閉程式、或是重開機。

當上述這些情況發生時當電腦只有一顆 CPU 可以執行 Process,那就會很明顯的被 Lock 住。
而為了解決 CPU 無法分頭進行處理就有了新的執行序(Thread)概念。

2、Thread(執行序)。

這時候就可以把當年準備考試的筆記拿出來用一下了。

當年上課老師也比喻 Thread 就像是一台車裡面有幾個引擎的概念。

Thread 與 Process 的差別。

Process Thread
OS 分配資源的對象單位 OS 分配 CPU 時間的對象單位
Process 之間無共享的資源及記憶體(除了 Shared memory 溝通外) 同一個 Process 內的 threads 彼此共享此 Process 的記憶體與 OS 資源
Process 的 Ceation、Context Switching 較慢,管理成本高。 Thread 的較快、管理成本低。
Process 內的 Single-thread 若 Blocked ,則整個 Process 也一起 Blocked Process 內只要有 Thread 還可以執行,則 Process 不會被 Blocked
Multiprocessor 架構之效益發揮較差。 較佳。
不需提供互斥存取機制(除了 Shared memory 溝通外) 必須對 Threads 共享的資源及 Data,提供互斥存取,控制防止資料不正確等問題。
適用時機:一個時間只有一個 Task 執行時 EX:Command interpreter、Linux Shell。 適用時機:一個時間有多個 Task 同時執行 EX:Clent-Server 架構的 Server。

所以透過多個執行序的概念我們希望程式能夠達到這樣。
(圖片取自 .NET 本事-非同步程式設計)

3、議題。

a、Context Swithcing。
解釋:
當 Cpu 要從執行中的 Process 切換給另一個 Process 執行時,kernel 必須先保存執中的 Process 狀態資訊且要載入另一個 Process 的狀態資訊。

所以我們知道其實相互切換是很浪費時間的,所以解決 Context Swithcing 消耗過多資源的其中一個方式就是使用 MultiThreading 技術。

b、Starvation。
每個執行序都有其優先順序,而當高優先權的執行序搶走大部分資源時就有可能造成 Starvation。
解釋:
Process 因為長期無法取的完工所需的各式資源,以至於遲遲無法完成工作,本身形成"Indefinite Blocking(不明確的)“現象。

4、相關名詞。

a、並行(Concurrency):
一次處理多件工作。Ex:使用者輸入文字,應用程式在背景執行拼字檢查。
b、多執行序(MultiThreading):
以多執行序的方式來實現並行(Concurrency)。
c、平行處理(Parallel processing):
把工作切分成小單元、交給多條Thread來同時執行。
d、非同步處理(Asunchronous processing):
是並行(Concurrency)的一種形式,但不必然(甚至是避免)使用 Thread,而是採用承諾(Promise)或 Callback event 的形式來達到並行。

5、非同步。

我們使用非同步的目的就是:

  • a、提升應用程式的回應速度。
  • b、提升應用程式整體產能,多執行序的應用程式更能善用 CPU 的運算能力。

二、如何使用非同步。

2021/03/22 今天發現對 Task 理解好像有點問題,回來補上作法。

在 .NET 裡面會使用到一個 Task 工作。
而 Task 會使用到 async、await 來互相搭配(通常一個 async 至少會搭配一個 await)。
拿個簡單的範例來看一下:

        static async Task Main(string[] args)
        {
            Console.WriteLine($"開始時間:{DateTime.Now.Second}");
            var milk = await GetMilkAsync();

            Console.WriteLine(milk);
            Console.WriteLine($"拿到牛奶的時間:{DateTime.Now.Second}");

            var toast = await GetToastAsync();
            Console.WriteLine(toast);
            Console.WriteLine($"拿到吐司的時間:{DateTime.Now.Second}");

            var egg = await GetEggAsync();
            Console.WriteLine(egg);
            Console.WriteLine($"拿到雞蛋的時間:{DateTime.Now.Second}");
            Console.WriteLine($"結束時間:{DateTime.Now.Second}");
        }

        static async Task<string> GetMilkAsync()
        {
            await Task.Delay(1000);
            return await Task.FromResult("Milk");
        }

        static async Task<string> GetToastAsync()
        {
            await Task.Delay(2000);
            return await Task.FromResult("Toast");
        }

        static async Task<string> GetEggAsync()
        {
            await Task.Delay(2000);
            return await Task.FromResult("Egg");
        }

結果總花費5秒:

如果是同步方法要取用非同步方法,請善用三件套。
ConfigureAwait(false).GetAwaiter().GetResult()

        static void Main(string[] args)
        {
            Console.WriteLine($"開始時間:{DateTime.Now.Second}");
            var milk = GetMilkAsync().ConfigureAwait(false).GetAwaiter().GetResult();

            Console.WriteLine(milk);
            Console.WriteLine($"拿到牛奶的時間:{DateTime.Now.Second}");

            var toast = GetToastAsync().ConfigureAwait(false).GetAwaiter().GetResult();
            Console.WriteLine(toast);
            Console.WriteLine($"拿到吐司的時間:{DateTime.Now.Second}");

            var egg = GetEggAsync().ConfigureAwait(false).GetAwaiter().GetResult();
            Console.WriteLine(egg);
            Console.WriteLine($"拿到雞蛋的時間:{DateTime.Now.Second}");
            Console.WriteLine($"結束時間:{DateTime.Now.Second}");
        }

        static async Task<string> GetMilkAsync()
        {
            await Task.Delay(1000);
            return await Task.FromResult("Milk");
        }

        static async Task<string> GetToastAsync()
        {
            await Task.Delay(2000);
            return await Task.FromResult("Toast");
        }

        static async Task<string> GetEggAsync()
        {
            await Task.Delay(2000);
            return await Task.FromResult("Egg");
        }

結果總花費5秒:

Task.WhenAll()

用法1、可以等待同質性的工作一起回來,使用場景 EX:當對方的 API 只提供一次撈取一個資料時,就可以使用 WhenAll 的方式等我們需要的資料全部撈完後再繼續動作。

        static async Task Main(string[] args)
        {
            Task<int> task1 = Task.FromResult(1);
            Task<int> task2 = Task.FromResult(2);
            Task<int> task3 = Task.FromResult(3);

            var result = await Task.WhenAll<int>(task1, task2, task3);
            Console.WriteLine(result.Length);
            Console.ReadLine();
        }

結果:

用法2、或是可以一起等待不同 Task 回來。

        private static async Task Main(string[] args)
        {
            Console.WriteLine($"開始時間:{DateTime.Now.Second}");
            var milkTask = GetMilkAsync();
            var toastTask = GetToastAsync();
            var eggTask = GetEggAsync();

            await Task.WhenAll(milkTask, toastTask, eggTask);

            var milkResult = await milkTask;
            var toastResult = await toastTask;
            var eggResult = await eggTask;
            Console.WriteLine(milkResult);
            Console.WriteLine($"拿到牛奶的時間:{DateTime.Now.Second}");
            Console.WriteLine(toastResult);
            Console.WriteLine($"拿到吐司的時間:{DateTime.Now.Second}");
            Console.WriteLine(eggResult);
            Console.WriteLine($"拿到雞蛋的時間:{DateTime.Now.Second}");
            Console.WriteLine($"結束時間:{DateTime.Now.Second}");
        }

        static async Task<string> GetMilkAsync()
        {
            await Task.Delay(1000);
            return await Task.FromResult("Milk");
        }

        static async Task<string> GetToastAsync()
        {
            await Task.Delay(2000);
            return await Task.FromResult("Toast");
        }

        static async Task<string> GetEggAsync()
        {
            await Task.Delay(3000);
            return await Task.FromResult("Egg");
        }

結果總花費3秒:

明顯看出與先前一個步驟一個步驟 await 等結果回來不同,因為任務是一起出去執行的,可以減少等待執行的時間。
提醒:這種使用方法就必須注意程式執行的先後順序,若使用到的地方在還沒回來前就被叫用,就有可能出錯。

後記

這邊僅列出三種比較常看到的用法,最後還是得強調,非同步的水很深,需要謹慎使用。
接下來我們會進行修改先前 API 把它改成非同步的模式。
那麼非同步基礎介紹就到此告一個段落。

推薦延伸閱讀: