参考:Taskcancellation in C# and things you should know about it 微软官方有一篇内容差不多的:Cancellation in Managed Threads
取消一个 Task 的步骤:
- 创建 CancellationToken(下文中以 token 表示),并将其传入 Task
- Task 内时刻监视 token 的取消请求(CancellationRequested)
- Task 检测到取消请求就做出响应
- 在响应的最后 token.ThrowIfCancellationRequested() 抛出 OperationCanceledException
- 在响应的最后 token.ThrowIfCancellationRequested() 抛出 OperationCanceledException
- 调用线程对任务取消进行响应
- 使用 try catch 捕获 OperationCanceledException
- 使用 try catch 捕获 OperationCanceledException
.NET 给我们提供了两个类用于取消 Task:
- CancellationTokenSource:负责创建 token 实例和发送取消请求给从它创建的所有 token 实例
- CancellationToken:传播取消请求
如何传递 token
下面先展示最简单(也是最没用)的方法。
CancellationTokenSource tokenSource = new CancellationTokenSource();
CancellationToken token = tokenSource.Token;
tokenSource.Cancel();
Task.Run(() => Console.WriteLine("Hello from Task"), token);
这种情况 Task.Run 只会在开始任务前检查一下 token。
如果希望在 Task 执行过程中取消它,我们需要将 token 彻底传入 Task。
两种方法:
使用全局变量或变量获取,使得 token 在 Task 内也可见
var token = tokenSource.Token;
Task.Run(() =>
{
var capturedToken = token;
...
}, token);
将 token 作为状态对象传递,并在 Task 内再转换为 token
var token = tokenSource.Token;
Task.Factory.StartNew(stateObject =>
{
var castedToken = (CancellationToken) stateObject;
...
}, token, token);
如何监控 token
现在我们在任务内部可以读取 token 了,接下来要关心的就是如何监控 token 的状态。
三种方式:
By polling:轮询 token.IsCancellationRequested
Task.Run(() =>
{
//Polling boolean property
while (!token.IsCancellationRequested) {
//Do some work
}
//Release resources and exit
}, token);
Callback registration:注册一旦取消请求被发出就执行的回调方法
Task.Run(async () =>
{
var webClient = newWebClient();
//Registering callback that would cancel downloading
token.Register(() => webClient.CancelAsync());
var data = await webClient.DownloadDataTaskAsync("http://www.google.com/");
}, token);
等待 token.WaitHandle
- WaitHandle 将在 token 取消时发出信号
- WaitHandle.WaitAny 可以等待并接收它发出的信号
var autoResetEvent = newAutoResetEvent(false);
//Some code...
Task.Run(() =>
{
var handleArray = new[] {autoResetEvent, token.WaitHandle};
//Waiting on wait handle to signal first
var finishedId = WaitHandle.WaitAny(handleArray);
if (finishedId == Array.IndexOf(handleArray, token.WaitHandle)){
//If token wait handle was first to signal then exit
return;
}
//Continue working...
}, token);
- WaitHandle 将在 token 取消时发出信号
注:可以通过创建 LinkedTokenSource 同时监控多个 token。
CancellationTokenSource linkedToukenSource = CancellationTokenSource.CreateLinkedTokenSource(firstToken, secondToken);
CancellationToken lindkedToken = linkedToukenSource.Token;
如何取消 Task
知道如何传递和监控 token 后,剩下的就是如何发送取消请求彻底取消 Task 了。
几种发送 cancellation 请求的方法:
//#1 在一定时间后发送取消请求
tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2));
//#2 在一定时间后发送取消请求
tokenSource.CancelAfter(TimeSpan.FromSeconds(2));
//#3 直接发送取消请求
tokenSource.Cancel();
//or
tokenSource.Cancel(true);
当我们取消任务时,它会隐式调用当前线程所有注册了的回调函数。如果回调函数抛出了异常,这些异常将被包装为一个 AggregateException(异常集合,里面有许多子异常)并将其抛给调用者。
因为回调函数是依次执行的,所以通过 Cancel 可选的 bool 参数,你可以指定这些回调函数遇到异常时的行为。
- true:异常马上抛出,后面的回调函数不再执行
false:所有注册了的回调函数都执行,它们抛出的异常包装为一个AggregateException
当在 task 内检测到 cancellation request 后,通常的做法是抛出 OperationCanceledException,让 task 进入 canceled 状态。Task.Run(() =>
{
while (true)
{
//Do some work
if (token.IsCancellationRequested)
{
token.ThrowIfCancellationRequested();
}
}
}, token);