C#CancellationToken/CancellationTokenSource-取消令牌/取消令牌源 CT/CTS
详细情况: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();