Asp.Net Core 輕鬆學-多執行緒之取消令牌
前言
取消令牌(CancellationToken) 是 .Net Core 中的一項重要功能,正確併合理的使用 CancellationToken 可以讓業務達到簡化程式碼、提升服務效能的效果;當在業務開發中,需要對一些特定的應用場景進行深度干預的時候,CancellationToken 將發揮非常重要的作用。
1. 多執行緒請求合併資料來源
在一個很常見的業務場景中,比如當請求一個文章詳細資訊的時候,需要同時載入部分點贊使用者和評論內容,這裡一共有 3 個任務,如果按照常規的先請求文章資訊,然後再執行請求點贊和評論,那麼我們需要逐一的按順序去資料庫中執行 3 次查詢;但是利用 CancellationToken ,我們可以對這 3 個請求同時執行,然後在所有資料來源都請求完成的時候,將這些資料進行合併,然後輸出到客戶端
1.1 合併請求文章資訊
public static void Test() { Random rand = new Random(); CancellationTokenSource cts = new CancellationTokenSource(); List<Task<Article>> tasks = new List<Task<Article>>(); TaskFactory factory = new TaskFactory(cts.Token); foreach (var t in new string[] { "Article", "Post", "Love" }) { Console.WriteLine("開始請求"); tasks.Add(factory.StartNew(() => { var article = new Article { Type = t }; if (t == "Article") { article.Data.Add("文章已載入"); } else { for (int i = 1; i < 5; i++) { Thread.Sleep(rand.Next(1000, 2000)); Console.WriteLine("load:{0}", t); article.Data.Add($"{t}_{i}"); } } return article; }, cts.Token)); } Console.WriteLine("開始合併結果"); foreach (var task in tasks) { Console.WriteLine(); var result = task.Result; foreach (var d in result.Data) { Console.WriteLine("{0}:{1}", result.Type, d); } task.Dispose(); } cts.Cancel(); cts.Dispose(); Console.WriteLine("\nIsCancellationRequested:{0}", cts.IsCancellationRequested); }
上面的程式碼定義了一個 Test() 方法,在方法內部,首先定義了一個 CancellationTokenSource 物件,該退出令牌源內部建立了一個取消令牌屬性 Token ;接下來,使用 TaskFacory 任務工廠建立了 3 個並行任務,並把這個任務存入 List<Task
> 列表物件中,在任務開始後,馬上迭代 tasks 列表,通過同步獲取每個任務的執行 Result 結果,在取消令牌沒有收到取消通知的時候,任務將正常的執行下去,在所有任務都執行完成後,將 3 個請求結果輸出到控制檯中,同時銷燬任務釋放執行緒資源;最後,執行 cts.Cancel()取消令牌並釋放資源,最後一句程式碼將輸出令牌的狀態。
1.2 執行程式,輸出結果
通過上面的輸出介面,可以看出,紅色部分是模擬請求,這個請求時多執行緒進行的,Post 和 Love 交替出現,是因為在程式中通過執行緒休眠的方式模擬網路阻塞過程,藍色為合併結果部分,可以看到,雖然“文章資訊”已經載入完成,但是因為 Post 和 Love 還在請求中,由於取消令牌未收到退出通知,所以合併結果會等待訊號,在所有執行緒都執行完成後,通過 cts.Cancel() 通知令牌取消,所有事件執行完成,控制檯列印結果黃色部分為令牌狀態,顯示為 True ,令牌已取消。
2. 對長時間阻塞呼叫的非同步取消令牌應用
在某些場景中,我們需要請求外部的第三方資源,比如請求天氣預報資訊;但是,由於網路等原因,可能會造成長時間的等待以致業務超時退出,這種情況可以使用 CancellationToken 來進行優化,但請求超過指定時長後退出,而不必針對每個 HttpClient 進行單獨的超時設定
2.1 獲取天氣預報
public async static Task GetToday()
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(3000);
HttpClient client = new HttpClient();
var res = await client.GetAsync("http://www.weather.com.cn/data/sk/101110101.html", cts.Token);
var result = await res.Content.ReadAsStringAsync();
Console.WriteLine(result);
cts.Dispose();
client.Dispose();
}
在上面的程式碼中,首先定義了一個 CancellationTokenSource 物件,然後馬上發起了一個 HttpClient 的 GetAsync 請求(注意,這種使用 HttpClient 的方式是不正確的,詳見我的部落格 HttpClient的演進和避坑 ;在 GetAsync 請求中傳入了一個取消令牌,然後立即發起了退出請求 Console.WriteLine(result); 不管 3 秒後請求是否返回,都將取消令牌等待訊號,最後輸出結果釋放資源
- 注意:如果是因為取消令牌退出引起請求中斷,將會丟擲任務取消的異常 TaskCanceledException
- 執行程式輸出結果
3. CancellationToken 的鏈式反應
可以使用建立一組令牌,通過連結各個令牌,使其建立通知關聯,當 CancellationToken 鏈中的某個令牌收到取消通知的時候,由鏈式中創建出來的 CancellationToken 令牌也將同時取消
3.1 建立鏈式測試程式碼
public async static Task Test()
{
CancellationTokenSource cts1 = new CancellationTokenSource();
CancellationTokenSource cts2 = new CancellationTokenSource();
var cts3 = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
cts1.Token.Register(() =>
{
Console.WriteLine("cts1 Canceling");
});
cts2.Token.Register(() =>
{
Console.WriteLine("cts2 Canceling");
});
cts2.CancelAfter(1000);
cts3.Token.Register(() =>
{
Console.WriteLine("root Canceling");
});
var res = await new HttpClient().GetAsync("http://www.weather.com.cn/data/sk/101110101.html", cts1.Token);
var result = await res.Content.ReadAsStringAsync();
Console.WriteLine("cts1:{0}", result);
var res2 = await new HttpClient().GetAsync("http://www.weather.com.cn/data/sk/101110101.html", cts2.Token);
var result2 = await res2.Content.ReadAsStringAsync();
Console.WriteLine("cts2:{0}", result2);
var res3 = await new HttpClient().GetAsync("http://www.weather.com.cn/data/sk/101110101.html", cts3.Token);
var result3 = await res2.Content.ReadAsStringAsync();
Console.WriteLine("cts3:{0}", result3);
}
上面的程式碼定義了 3 個 CancellationTokenSource ,分別是 cts1,cts2,cts3,每個 CancellationTokenSource 分別註冊了 Register 取消回撥委託,然後,使用 HttpClient 發起 3 組網路請求;其中,設定 cts2 在請求開始 1秒 後退出,預期結果為:當 cts2 退出後,由於 cts3 是使用 CreateLinkedTokenSource(cts1.Token, cts2.Token) 創建出來的,所以 cts3 應該也會被取消,實際上,無論 cts1/cts2 哪個令牌取消,cts3 都會被取消
3.2 執行程式,輸出結果
從上圖可以看到,紅色部分輸出結果是:首先 cts2 取消,接著產生了鏈式反應導致 cts3 也跟著取消,藍色部分為 cts1 的正常請求結果,最後輸出了任務退出的異常資訊
4. CancellationToken 令牌取消的三種方式
CancellationToken 定義了三種不同的取消方法,分別是 Cancel(),CancelAfter(),Dispose();這三種方式都代表了不同的行為方式
4.1 演示取消動作
public static void Test()
{
CancellationTokenSource cts1 = new CancellationTokenSource();
cts1.Token.Register(() =>
{
Console.WriteLine("\ncts1 ThreadId: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
});
cts1.Cancel();
Console.WriteLine("cts1 State:{0}", cts1.IsCancellationRequested);
CancellationTokenSource cts2 = new CancellationTokenSource();
cts2.Token.Register(() =>
{
Console.WriteLine("\ncts2 ThreadId: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
});
cts2.CancelAfter(500);
System.Threading.Thread.Sleep(1000);
Console.WriteLine("cts2 State:{0}", cts2.IsCancellationRequested);
CancellationTokenSource cts3 = new CancellationTokenSource();
cts3.Token.Register(() =>
{
Console.WriteLine("\ncts3 ThreadId: {0}", System.Threading.Thread.CurrentThread.ManagedThreadId);
});
cts3.Dispose();
Console.WriteLine("\ncts3 State:{0}", cts3.IsCancellationRequested);
}
4.2 執行程式,輸出結果如下
上面的程式碼定義了 3 個 CancellationTokenSource,分別是 cts1/cts2/cts3;分別執行了 3 中不同的取消令牌的方式,並在取消回撥委託中輸出執行緒ID,從輸出介面中看出,當程式執行 cts1.Cancel() 方法後,取消令牌立即執行了回撥委託,並輸出執行緒ID為:1;cts2.CancelAfter(500) 表示 500ms 後取消,為了獲得令牌狀態,這裡使執行緒休眠了 1000ms,而 cts3 則直接呼叫了 Dispose() 方法,從輸出結果看出,cts1 執行在和 Main 方法在同一個執行緒上,執行緒 ID 都為 1,而 cts2 由於使用了延遲取消,導致其在內部新建立了一個執行緒,其執行緒 ID 為 4;最後,cts3由於直接呼叫了 Dispose() 方法,但是其 IsCancellationRequested 的值為 False,表示未取消,而輸出結果也表明,沒有執行回撥委託
結束語
- 通過本文,我們學習到了如何在不同的應用場景下使用 CancellationToken
- 掌握了合併請求、中斷請求、鏈式反應 三種使用方式
- 最後還了解到三種不同的取消令牌方式,知道了各種不同取消方式的區別