取消令牌(CancellationToken) 是 .Net Core 中的一項重要功能,正確併合理的使用 CancellationToken 可讓業務達到簡化代碼、提高服務性能的效果;當在業務開發中,須要對一些特定的應用場景進行深度干預的時候,CancellationToken 將發揮很是重要的做用。html
在一個很常見的業務場景中,好比當請求一個文章詳細信息的時候,須要同時加載部分點贊用戶和評論內容,這裏一共有 3 個任務,若是按照常規的先請求文章信息,而後再執行請求點贊和評論,那麼咱們須要逐一的按順序去數據庫中執行 3 次查詢;可是利用 CancellationToken ,咱們能夠對這 3 個請求同時執行,而後在全部數據源都請求完成的時候,將這些數據進行合併,而後輸出到客戶端git
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()取消令牌並釋放資源,最後一句代碼將輸出令牌的狀態。 github
經過上面的輸出接口,能夠看出,紅色部分是模擬請求,這個請求時多線程進行的,Post 和 Love 交替出現,是由於在程序中經過線程休眠的方式模擬網絡阻塞過程,藍色爲合併結果部分,能夠看到,雖然「文章信息」已經加載完成,可是由於 Post 和 Love 還在請求中,因爲取消令牌未收到退出通知,因此合併結果會等待信號,在全部線程都執行完成後,經過 cts.Cancel() 通知令牌取消,全部事件執行完成,控制檯打印結果黃色部分爲令牌狀態,顯示爲 True ,令牌已取消。數據庫
在某些場景中,咱們須要請求外部的第三方資源,好比請求天氣預報信息;可是,因爲網絡等緣由,可能會形成長時間的等待以至業務超時退出,這種狀況可使用 CancellationToken 來進行優化,但請求超過指定時長後退出,而沒必要針對每一個 HttpClient 進行單獨的超時設置網絡
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
可使用建立一組令牌,經過連接各個令牌,使其創建通知關聯,當 CancellationToken 鏈中的某個令牌收到取消通知的時候,由鏈式中建立出來的 CancellationToken 令牌也將同時取消dom
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 都會被取消異步
從上圖能夠看到,紅色部分輸出結果是:首先 cts2 取消,接着產生了鏈式反應致使 cts3 也跟着取消,藍色部分爲 cts1 的正常請求結果,最後輸出了任務退出的異常信息async
CancellationToken 定義了三種不一樣的取消方法,分別是 Cancel(),CancelAfter(),Dispose();這三種方式都表明了不一樣的行爲方式性能
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); }
上面的代碼定義了 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,表示未取消,而輸出結果也代表,沒有執行回調委託
https://github.com/lianggx/EasyAspNetCoreDemo/tree/master/Ron.ThreadingDemo