發現問題
你點了外賣後,會一直不做其它事情,一直等外賣的到來麼?
當然不會拉!
我們來看看程式碼世界的:
public void Query(){
// 當前執行緒 向 資料庫伺服器 發起查詢命令
// 在 資料庫伺服器 返回資料之前,當前執行緒 一直等待,不幹活了!!!
var data = Database.Query();
}
假設在一個請求響應中:
- 執行緒用 5ms 來驗證使用者的輸入的引數;
- 執行緒用 50ms 來等待資料庫返回;
- 執行緒用 5ms 序列化資料響應返回給使用者;
可以看到在 60ms 中,執行緒摸魚 50ms。
而很多Web框架,收到一個請求,就會建立一個執行緒來處理,
如果片刻間內有100個使用者請求這個方法,那麼就得安排100個執行緒,
有沒有方法讓第1個執行緒在等待資料返回時,先去接待第N+1個使用者(校驗請求引數什麼的)
這樣就能大大減少執行緒數量~
透過上面的例子,我相信你已有所悟:非同步就是避免讓執行緒摸魚。
概念與理論
接下來為了更有效地溝通和提示逼格,我們還是使用專業的術語。
複習一下執行緒的阻塞,睡眠,掛起。
主要是弄明白阻塞的定義,和什麼時候會發生阻塞。
執行緒阻塞:
Thread t = new Thread(()=>{
// 阻塞:執行緒 被動 地等待外部返回,才能繼續執行
var resp = Http.Get(url); // 需要等待網路傳輸檔案
});
執行緒睡眠:
Thread t = new Thread(()=>{
// 睡眠:執行緒 主動 停止執行片刻,然後繼續執行
Thread.Sleep(1000);
});
執行緒掛起:
// 虛擬碼,C# 的 ThreadPool 沒有這些方法
// 主動叫執行緒去休息
ThreadPool.Recycle(t)
// 等到有工作了,再叫執行緒處理執行
t = ThreadPool.GetThread();
t.Run(fun);
Synchronous(同步):
本人對 同步 給出比較容易理解的定義是:按順序步驟,一個步驟只做一件事情。
本人以前看到 同步 這個詞,錯誤地顧名思義,以為是同一刻時間做幾件事,錯錯錯!!!
// 執行緒會一步一步執行以下程式碼,這個過程叫 同步
// 先發完簡訊
SMS.Send(msg); // 2秒
// 再發郵件
Email.Send(smg); // 1秒
// 總耗時 3秒
Parallel(並行):
指兩個或兩個以上事件(或執行緒)在同一時刻發生。
// 分別建立兩個執行緒並行去執行,誰也不用等待誰~
Thread t1 = new Thread(()=>{
SMS.Send(msg); // 2秒
});
// t2 執行緒不需要等待 t1 執行緒
Thread t2 = new Thread(()=>{
Email.Send(smg); // 1秒
});
// 總耗時 2秒
微軟官方檔案-使用 Async 和 Await 的非同步程式設計
微軟用的做早餐的例子:
- 倒一杯咖啡。
- 加熱平底鍋,然後煎兩個雞蛋。
- 煎三片培根。
- 烤兩片面包。
- 在烤麵包上加黃油和果醬。
- 倒一杯橙汁。
同步則是單人(單執行緒)從 1 到 6 一步一步地做 —— 效率低。
並行則是多人(多執行緒),一人倒咖啡;一人煎雞蛋;一個...同時進行 —— 效率高,人力成本高。
非同步則是單人(單執行緒),點火熱平底鍋,平底鍋要等待變熱,那麼先把麵包放進烤麵包機...
Asynchronous(非同步):
指的是,當執行緒遇到阻塞時,讓執行緒先去執行其它工作~
我們應該體驗過,當一個人要在很多事情上來回切換的時候,很容易出錯。
做早餐,我們點火熱平底鍋後就去烤麵包,但平底鍋什麼時候好,我們什麼時候切換回來煎雞蛋,還是去倒橙汁。
要將程式碼的執行過程寫成非同步的,也不是容易的事情。
好在 C# 提供 async
和 await
這兩個關鍵字,
輕鬆建立非同步方法(幾乎與建立同步方法一樣輕鬆) —— 微軟官方檔案原話
理論講解完畢,是時候來實踐了~
async 修飾符
public void Get()
{
// 這是一個 同步方法
// 如果這個內部有會發生阻塞的功能程式碼,比如讀取網路資源,
// 那麼一個執行緒執行這個方法遇到阻塞,這個執行緒就會摸魚~
}
要將一個同步方法宣告為非同步方法,首先需要將用 async
修飾符標記一下,
public async void Get()
{
// 這是一個 非同步方法
// 如果這個內部有會發生阻塞的功能程式碼
// 那麼一個執行緒執行這個方法遇到阻塞時,這個執行緒就會去做其它事情~
}
public async void Get()
{
HttpClient httpClient = new HttpClient();
httpClient.GetAsync("https://learn.microsoft.com/zh-cn/docs/");
}
加入一些我們需要觀察的程式碼後,得:
public static void Main()
{
Console.WriteLine($"Main 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
Get();
Console.WriteLine($"Main 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
// 這程式碼是有問題的,我有意為之,用來和接下來的更完善的程式碼做比較~
public static async void Get()
{
Console.WriteLine($"Get 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
HttpClient httpClient = new HttpClient();
httpClient.GetAsync("https://learn.microsoft.com/zh-cn/docs/");
Console.WriteLine($"Get 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
}
執行後的控制檯輸出:
Main 開始執行前執行緒 Id:1
Get 開始執行前執行緒 Id:1
Get 執行結束後執行緒 Id:1
Main 執行結束後執行緒 Id:1
注意!!!這個時候方法雖然被宣告為非同步的,但現在執行過程還是同步的!!!!
await 運運算元
非同步方法同步執行,直至到達其第一個 await 表示式,此時會將方法掛起,直到等待的任務完成。
如果 async 關鍵字修改的方法不包含 await 表示式或語句,則該方法將同步執行。 編譯器警告將通知你不包含 await 語句的任何非同步方法,因為該情況可能表示存在錯誤。 請參閱編譯器警告(等級 1)CS4014。
所以完善的程式碼,應該是這樣子的:
public static void Main()
{
Console.WriteLine($"Main 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
Get(); // Get 方法雖然是宣告為非同步的,但依舊時同步執行
Console.WriteLine($"Main 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
public static async void Get()
{
Console.WriteLine($"Get 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
HttpClient httpClient = new HttpClient();
// 加上 await 運運算元,才是真正的非同步執行!!!
await httpClient.GetAsync("https://learn.microsoft.com/zh-cn/docs/");
Console.WriteLine($"Get 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
}
執行後的控制檯輸出:
Main 開始執行前執行緒 Id:1 # 執行緒1,進入 main 函式
Get 開始執行前執行緒 Id:1 # 執行緒1,執行 Get 函式,遇到阻塞,但執行緒1被要求不能摸魚,
Main 執行結束後執行緒 Id:1 # 於是看看有沒有其它工作做,發現需要列印...
Get 執行結束後執行緒 Id:9 # 阻塞結束後,誰來執行剩下的程式碼呢?
# 如果執行緒1有空,可以回來執行,如果執行緒1忙,則有其它執行緒接管
# 由排程分配決定
我們自己定義的非同步方法 Get()
和呼叫非同步方法 httpClient.GetAsync
,
只有 httpClient.GetAsync
是非同步執行的。
也就是說單單使用 async
還不夠,還得必須同時使用 await
Task 類
通常來說,我們使用 httpClient.GetAsync
,都是希望能處理返回的資料。
- Task 表示不返回值且通常非同步執行的單個操作。
- Task<TResult> 表示返回值且通常非同步執行的單個操作。
- void 對於除事件處理程式以外的程式碼,通常不鼓勵使用 async void 方法,因為呼叫方不能 await 那些方法,並且必須實現不同的機制來報告成功完成或錯誤條件。
public static async void Get()
{
const string url = "https://learn.microsoft.com/zh-cn/docs/";
Console.WriteLine($"Get 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
HttpClient httpClient = new HttpClient();
// 用 Task 來 = 一個非同步操作
Task<HttpResponseMessage> taskResp = httpClient.GetAsync(url);
HttpResponseMessage resp = await taskResp;// 等待非同步操作完成返回
// 可以對 resp 進行一些處理
Console.WriteLine($"Get 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
}
上面程式碼可以簡化為:
public static async void Get()
{
const string url = "https://learn.microsoft.com/zh-cn/docs/";
Console.WriteLine($"Get 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
HttpClient httpClient = new HttpClient();
HttpResponseMessage resp = await httpClient.GetAsync(url);
Console.WriteLine($"Get 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
}
多個Task 的例子:
public static async void Get()
{
Console.WriteLine($"Get 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
HttpClient httpClient = new HttpClient();
var t1 = httpClient.GetAsync("https://learn.microsoft.com/");
var t2 = httpClient.GetAsync("https://cn.bing.com/");
var t3 = httpClient.GetAsync("https://www.cnblogs.com/");
Console.WriteLine($"Get await 之前的執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
await Task.WhenAll(t1, t2, t3); // 等待多個非同步任務完成
//Task.WaitAll(t1, t2, t3);
//await Task.Yield();
//await Task.Delay(0);
Console.WriteLine($"Get 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
}
執行後的控制檯輸出:
Main 開始執行前執行緒 Id:1
Get 開始執行前執行緒 Id:1
Get await 之前的執行緒 Id:1
Main 執行結束後執行緒 Id:1
Get 執行結束後執行緒 Id:14
按微軟官方檔案的建議和規範的最終版本:
public static void Main()
{
Console.WriteLine($"Main 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
GetAsync().Wait();
Console.WriteLine($"Main 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
Console.ReadKey();
}
// 通常不鼓勵使用 async void 方法
// 非同步方法名約定以 Async 結尾
public static async Task GetAsync()
{
Console.WriteLine($"Get 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
HttpClient httpClient = new HttpClient();
var t1 = httpClient.GetAsync("https://learn.microsoft.com/");
var t2 = httpClient.GetAsync("https://cn.bing.com/");
var t3 = httpClient.GetAsync("https://www.cnblogs.com/");
Console.WriteLine($"Get await 之前的執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
Task.WaitAll(t1, t2, t3); // 等待多個非同步任務完成
await Task.Yield();
//await Task.Delay(0);
Console.WriteLine($"Get 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
}
執行後的控制檯輸出:
Main 開始執行前執行緒 Id:1
Get 開始執行前執行緒 Id:1
Get await 之前的執行緒 Id:1
Get 執行結束後執行緒 Id:5
Main 執行結束後執行緒 Id:1
測試
public static async Task GetAsync()
{
Console.WriteLine($"Get 開始執行前執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
Stopwatch sw = new Stopwatch();
sw.Start();
TestHttp(); // http 網路不穩定,不好觀察時間,可以試試 TestIdle()
sw.Stop();
Console.WriteLine($"一共耗時:{sw.ElapsedMilliseconds} 毫秒");
Console.WriteLine($"Get 執行結束後執行緒 Id:{Thread.CurrentThread.ManagedThreadId}");
await Task.Yield();
}
public static void TestHttp()
{
HttpClient httpClient = new HttpClient();
List<Task<HttpResponseMessage>> tasks = new List<Task<HttpResponseMessage>>();
for (int i = 0; i < 10; i++)
{
var t = httpClient.GetAsync("https://learn.microsoft.com/");
tasks.Add(t);
}
Task.WaitAll(tasks.ToArray());
foreach (var item in tasks)
{
var html = item.Result.Content.ReadAsStringAsync().Result;
}
}
public static void TestIdle()
{
List<Task> tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
var t = Idle();
tasks.Add(t);
}
Task.WaitAll(tasks.ToArray());
}
public static async Task Idle()
{
// 可以用於模擬阻塞效果
await Task.Delay(1000);
// 不能用 Sleep 來模擬阻塞,Sleep 不是阻塞,是睡眠
// Thread.Sleep(1000);
}
Main 開始執行前執行緒 Id:1
Get 開始執行前執行緒 Id:1
一共耗時:604 毫秒 # 1個執行緒幹了10個執行緒的活,時間還差不多,美滋滋~
Get 執行結束後執行緒 Id:1
Main 執行結束後執行緒 Id:1
至此,關於 C# 中非同步程式設計的三個知識點 async
,await
,Task
講解完畢。
在寫例子的過程中,
發現 HttpClient 這個類很多方法都是非同步方法了,
依稀記得以前還有同步方法和非同步方法提供選擇的,
看來微軟是在逼大家進步啊~
如果文章能幫到你,點個贊吧,十分感謝~
參考資料
非同步程式設計:
https://docs.microsoft.com/zh-cn/dotnet/csharp/async
使用 Async 和 Await 的非同步程式設計:
https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async
深入瞭解非同步:
https://docs.microsoft.com/zh-cn/dotnet/standard/async-in-depth
async 關鍵字:
https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/async
await 運運算元:
https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/await
Async/Await 非同步程式設計中的最佳做法:
https://docs.microsoft.com/zh-cn/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
Future 與 promise:
https://zh.wikipedia.org/wiki/Future與promise