前言
先前我們介紹了 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 把它改成非同步的模式。
那麼非同步基礎介紹就到此告一個段落。
推薦延伸閱讀: