詳細情況:https://www.cnblogs.com/wucy/p/15128365.html
背景
為什么引入取消令牌?
Thread.abort()方法會破壞同步鎖中代碼的原子邏輯,破壞鎖的作用。以下代碼說明了Thread.abort()方是如何破壞鎖的 :
代碼的功能:每個線程進入鎖內都會休息10s。
int a = 0; void RelaxMoment() { lock ("") { a++; if (a == 1) { Thread.Sleep(10000); a--; } } }
線程A正在鎖中sleep中突然被主線程abort停止了,此時a=1。直接導致后續進入鎖的線程無法休息。
既然終止一個線程不能使用abort方法,那怎樣才能終止一個正在運行的線程呢?答案也很簡單,使用自定義的標志位決定線程的執行情況,工作線程內程序員根據標志自行判讀該合理的停止的位置,所以就引入標志----取消令牌CancellationToken
CancellationTokenSource原理刨析
CTS是用來發出取消標記,線程和任務根據CTS發出的信號,在自行決定在合適的位置取消線程或任務。
CTS是發出取消標記方式有2種 自動和手動:
1、手動通過調用Cancled()發出取消標記。
2、自動方式通過定時器、指定時間后取消信號。
重要的內部成員
ManualResetEvent? _kernelEvent; :取消線程暫停
int _state :標記取消
屬性
IsCancellationRequested:判斷IsCancellationRequested => _state != NotCanceledState;
Token:封裝CTS,返回對CTS操縱源CT,俗稱取消令牌
方法
Cancel()
1、將取消標記(_state=1)設置為 "已取消" 狀態。 int _state=1; 2 、 TimerQueueTimer? timer.Close(); 取消隊列 3、 _kernelEvent?.Set(); //將內部的ManualResetEvent設置為set() 4、ExecuteCallbackHandlers(bool throwOnFirstException);//執行ct.Register(委托)中注冊的委托
CreateLinkedTokenSource():將多個CTS鏈接一起 形成新的CTS,相當於總開關。
CancelAfter(Int32/TimeSpan) :指定過多長時間后取消
Dispose():由於CTS內部有一個內核對象ManualResetEvent實例 所以需要釋放,一般GC會自動回收,不用調用該方法。
CancellationToken原理刨析
取消令牌(CT)只是用來封裝取消令牌源(CTS)的。線程/任務中通過CT接收CTS源發出的set信號或通過CT不停的偵聽CTS狀態,判斷是否發出取消消息。如果發出取消消息,那么線程/任務在自行決定在合適的位置取消線程或任務。
CT內部封裝了一個cts 實例的引用。CTS內部封裝了一個 ManualResetEvent 實例對象和int型取消狀態 。 線程/任務通過CT內部cts 對象發出的信號,來自行決定在合適的位置取消線程或任務
屬性
WaitHandle:返回cts.ManualResetEvent 實例。
None:default
IsCancellationRequested:cts.IsCancellationRequested => cts != null && cts.IsCancellationRequested; 然后可以拋出 ThrowOperationCanceledException();結束線程或任務。
CanBeCanceled:CanBeCanceled => _cts != null;
方法
ThrowIfCancellationRequested()源碼:
public void ThrowIfCancellationRequested() { if (IsCancellationRequested) ThrowOperationCanceledException(); }
Register():從這個最底層的方法我們可以得知,其本質還是調用CancellationTokenSource的InternalRegister方法,核心操作都不在CancellationToken還是在CancellationTokenSource類,源代碼如下:
private CancellationTokenRegistration Register(Delegate callback!!, object? state, bool useSynchronizationContext, bool useExecutionContext) { CancellationTokenSource? source = _source; return source != null ? source.Register(callback, state, useSynchronizationContext ? SynchronizationContext.Current : null, useExecutionContext ? ExecutionContext.Capture() : null) : default; // Nothing to do for tokens than can never reach the canceled state. Give back a dummy registration. }
CancellationTokenSource 取消任務用法詳解
CancellationTokenSource.Cancel不代表立即終止代碼。它只是通知工作線程 “你可以結束了”。工作線程在關鍵的代碼行中插入監控代碼,判斷任務是否被取消,這類似於軟件工程中的“埋點”,
用戶可以在何理的位置拋出異常結束線程。並且在取消任務時候通過CancellationToken.Register處理收尾工作。
if (ct.IsCancellationRequested) { Console.WriteLine("Task {0} cancelled", taskNum); ct.ThrowIfCancellationRequested(); //此方法提供等效於的功能: //if (token.IsCancellationRequested) //throw new OperationCanceledException(token); }
相關的類
CancellationTokenSource 類
CancellationToken 結構
CancellationTokenRegistration 結構
WaitHandle 類
ManualResetEvent 類
取消任務
1、定時取消
創建 CancellationTokenSource 的時候,可以傳入時間(毫秒或者Timespan), 通過它我們可以在等待一段時間后,自動取消任務。
CancellationTokenSource cts = new CancellationTokenSource(1000); _ = Execute(cts.Token); Console.ReadKey();
我們也可以調用 cts.CancelAfter(1000), 它會在1s后取消任務。
2、第二種方式定時取消任務
CancellationTokenSource tokenSource = new CancellationTokenSource(); CancellationToken cancellationToken = tokenSource.Token; cancellationToken.Register(() => System.Console.WriteLine("被取消了."));//要在取消 CancellationToken 時執行的委托。 tokenSource.CancelAfter(5000); while (true) { //如果操作被取消則直接拋出異常 cancellationToken.ThrowIfCancellationRequested(); System.Console.WriteLine("一直在執行..."); await Task.Delay(1000); } /* *輸出 一直在執行... 一直在執行... 一直在執行... 一直在執行... 一直在執行... 被取消了. */
int eventThatSignaledIndex =WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },new TimeSpan(0, 0, 20)); if (eventThatSignaledIndex == 1) { Console.WriteLine("The wait operation was canceled."); throw new OperationCanceledException(token); //其他代碼
4、關聯取消
將cts1、cts2、cts3多個取消令牌源關聯在一起ctsLink,形成新的取消令牌源。 只要cts1、cts2、cts3其中一個令牌發出取消命令,那么ctsLink取消。
//聲明幾個CancellationTokenSource CancellationTokenSource cts1 = new (); CancellationTokenSource cts2 = new (); CancellationTokenSource cts3 = new(); cts2.Token.Register(() => System.Console.WriteLine("tokenSource2被取消了")); cts1.Token.Register(() => System.Console.WriteLine("tokenSource1被取消了")); //創建一個關聯的CancellationTokenSource var ctsLink = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token, cts3.Token); ctsLink.Token.Register(() => System.Console.WriteLine("ctsLink被取消了")); //取消tokenSource2 cts3.Cancel(); //輸出 ctsLink被取消了
5、不允許被取消的操作
要執行一個不允許被取消的操作,可向該操作傳遞通過調用CancellationToken的靜態None屬性而返回的CancellationToken。
該屬性返回一個特殊的CancellationToken實例,它不和任何CancellationTokenSource對象關聯(實例的私有字段為null)。
由於沒有CancellationTokenSource,所以沒有代碼能調用Cancel .一個操作如果查詢這個特殊CancellationToken的 IsCancellationRequested屬性,將總是返回false.使用某個特殊 CancellationToken實例查詢CancellationToken的CanBeCanceled屬性,屬性會返回false。
相反,對於通過查詢CancellationTokenSource對象的 Token屬性而獲得的其他所有CancellationToken實例,該屬性(CanBeCanceled)都會返回 true.
CancellationToken 注冊回調
我們可以調用 Register()方法,注冊Token取消的回調,參數需要傳入 Action 委托。
CancellationTokenSource cts = new CancellationTokenSource(1000); cts.Token.Register(() => Console.WriteLine("任務已取消!")); // 開始異步任務 _ = Execute(cts.Token); Console.ReadKey();
Register() 注冊回調后,返回一個 CancellationTokenRegistration 對象,同樣的,你可以在回調函數執行前,移除注冊回調,就像這樣:
CancellationTokenSource tokenSource = new (); CancellationTokenRegistration cancellationTokenRegistration = tokenSource.Token.Register(() => Console.WriteLine("")); cancellationTokenRegistration.Unregister(); tokenSource.Cancel();