从 .NET Framework 4 开始,.NET 在协作式取消异步操作或长时间运行的同步操作时使用统一的模型。 此模型基于名为取消令牌的轻型对象。 调用一个或多个可取消操作的对象(例如,通过创建新线程或任务)将令牌传递给每个操作。 单个操作反过来可将标记的副本传递给其他操作。 稍后,创建令牌的对象可以使用它来请求停止正在进行的操作。 只有请求对象可以发出取消请求,并且每个侦听器负责通知请求并以适当且及时的方式响应请求。
实现协作取消模型的一般模式是:
实例化对象 CancellationTokenSource ,该对象管理取消通知并将其发送到单个取消令牌。
将 CancellationTokenSource.Token 属性返回的标记传递给每个侦听取消的任务或线程。
为每个任务或线程提供响应取消的机制。
CancellationTokenSource.Cancel调用该方法以提供取消通知。
重要
CancellationTokenSource 类实现 IDisposable 接口。 使用取消标记源释放所包含的任何非托管资源后,应确保调用 CancellationTokenSource.Dispose 方法。
下图显示了令牌源与其令牌的所有副本之间的关系。
借助协作取消模型,可以更轻松地创建可识别取消的应用程序和库,并支持以下功能:
取消具有协作性,且不会在侦听器上强制执行。 侦听器确定如何适当地以响应取消请求终止操作。
请求不同于倾听。 调用可取消操作的对象可以控制何时(是否会请求)取消。
请求对象通过一次方法调用向令牌的所有副本发送取消请求。
侦听器可以通过将它们联接到一个 链接令牌中来同时侦听多个令牌。
用户代码可以注意到和响应来自库代码的取消请求,库代码可以注意到和响应来自用户代码的取消请求。
侦听器可通过轮询、回调注册或等待等待句柄来接收到取消请求的通知。
取消类型
取消框架作为一组相关类型实现,下表中列出了这些类型。
类型名称 | DESCRIPTION |
---|---|
CancellationTokenSource | 创建取消令牌的对象,并针对该令牌的所有副本发出取消请求。 |
CancellationToken | 通常作为方法参数传递给一个或多个侦听器的轻量值类型。 侦听器通过轮询、回调或等待句柄监视标记的 IsCancellationRequested 属性的值。 |
OperationCanceledException | 此异常的构造函数的重载将 CancellationToken 作为参数接受。 侦听器可能会选择性地引发此异常,议验证取消源并通知其他侦听器它已响应取消请求。 |
.NET中集成了几种类型的取消模型。 最重要的一个是System.Threading.Tasks.Parallel,System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult>和System.Linq.ParallelEnumerable。 建议对所有新的库和应用程序代码使用此协作取消模型。
代码示例
在以下示例中,请求对象创建一个 CancellationTokenSource 对象,然后将其 Token 属性传递给可取消的操作。 接收请求的操作通过轮询监视标记的 IsCancellationRequested 属性的值。 当值变为 true
时,侦听器可以以适当的方式终止。 在此示例中,方法只需退出,很多情况下都只需执行此操作。
注释
示例使用 QueueUserWorkItem 方法演示协作取消框架与传统 API 的兼容性。 有关使用首选 System.Threading.Tasks.Task 类型的示例,请参阅 如何:取消任务及其子任务。
using System;
using System.Threading;
public class Example
{
public static void Main()
{
// Create the token source.
CancellationTokenSource cts = new CancellationTokenSource();
// Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
Thread.Sleep(2500);
// Request cancellation.
cts.Cancel();
Console.WriteLine("Cancellation set in token source...");
Thread.Sleep(2500);
// Cancellation should have happened, so call Dispose.
cts.Dispose();
}
// Thread 2: The listener
static void DoSomeWork(object? obj)
{
if (obj is null)
return;
CancellationToken token = (CancellationToken)obj;
for (int i = 0; i < 100000; i++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("In iteration {0}, cancellation has been requested...",
i + 1);
// Perform cleanup if necessary.
//...
// Terminate the operation.
break;
}
// Simulate some work.
Thread.SpinWait(500000);
}
}
}
// The example displays output like the following:
// Cancellation set in token source...
// In iteration 1430, cancellation has been requested...
Imports System.Threading
Module Example1
Public Sub Main1()
' Create the token source.
Dim cts As New CancellationTokenSource()
' Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)
Thread.Sleep(2500)
' Request cancellation by setting a flag on the token.
cts.Cancel()
Console.WriteLine("Cancellation set in token source...")
Thread.Sleep(2500)
' Cancellation should have happened, so call Dispose.
cts.Dispose()
End Sub
' Thread 2: The listener
Sub DoSomeWork(ByVal obj As Object)
Dim token As CancellationToken = CType(obj, CancellationToken)
For i As Integer = 0 To 1000000
If token.IsCancellationRequested Then
Console.WriteLine("In iteration {0}, cancellation has been requested...",
i + 1)
' Perform cleanup if necessary.
'...
' Terminate the operation.
Exit For
End If
' Simulate some work.
Thread.SpinWait(500000)
Next
End Sub
End Module
' The example displays output like the following:
' Cancellation set in token source...
' In iteration 1430, cancellation has been requested...
操作取消与对象取消
在协作取消框架中,取消是指操作,而不是对象。 取消请求意味着在完成任何所需的清理后,操作应尽快停止。 一个取消标记应代指一个“可取消操作”,但可在程序中实现此操作。 将 IsCancellationRequested 令牌的属性设置为 true
后,无法将其重置为 false
。 因此,取消令牌后,不能重复使用取消令牌。
如果需要对象取消机制,您可以通过调用 CancellationToken.Register 方法来基于操作取消机制,如以下示例所示。
using System;
using System.Threading;
class CancelableObject
{
public string id;
public CancelableObject(string id)
{
this.id = id;
}
public void Cancel()
{
Console.WriteLine($"Object {id} Cancel callback");
// Perform object cancellation here.
}
}
public class Example1
{
public static void Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
// User defined Class with its own method for cancellation
var obj1 = new CancelableObject("1");
var obj2 = new CancelableObject("2");
var obj3 = new CancelableObject("3");
// Register the object's cancel method with the token's
// cancellation request.
token.Register(() => obj1.Cancel());
token.Register(() => obj2.Cancel());
token.Register(() => obj3.Cancel());
// Request cancellation on the token.
cts.Cancel();
// Call Dispose when we're done with the CancellationTokenSource.
cts.Dispose();
}
}
// The example displays the following output:
// Object 3 Cancel callback
// Object 2 Cancel callback
// Object 1 Cancel callback
Imports System.Threading
Class CancelableObject
Public id As String
Public Sub New(id As String)
Me.id = id
End Sub
Public Sub Cancel()
Console.WriteLine("Object {0} Cancel callback", id)
' Perform object cancellation here.
End Sub
End Class
Module ExampleOb1
Public Sub MainOb1()
Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
' User defined Class with its own method for cancellation
Dim obj1 As New CancelableObject("1")
Dim obj2 As New CancelableObject("2")
Dim obj3 As New CancelableObject("3")
' Register the object's cancel method with the token's
' cancellation request.
token.Register(Sub() obj1.Cancel())
token.Register(Sub() obj2.Cancel())
token.Register(Sub() obj3.Cancel())
' Request cancellation on the token.
cts.Cancel()
' Call Dispose when we're done with the CancellationTokenSource.
cts.Dispose()
End Sub
End Module
' The example displays output like the following:
' Object 3 Cancel callback
' Object 2 Cancel callback
' Object 1 Cancel callback
如果对象支持多个并发可取消作,请将单独的令牌作为输入传递给每个不同的可取消作。 这样,就可以取消一个操作,而不会影响其他操作。
倾听和响应取消请求
在用户代理中,可取消操作的实现者确定如何以响应取消请求终止操作。 在很多情况下,用户委托只需执行全部所需清理,然后立即返回。
但是,在更复杂的情况下,可能需要用户代理通知库代码,说明取消已发生。 在这种情况下,终止操作的正确方式是委托调用 ThrowIfCancellationRequested 方法,这将引发 OperationCanceledException。 库代码可以在用户委托线程上捕获此异常,并检查异常的令牌,以确定异常指示协作取消还是某些其他异常情况。
类 Task 以这种方式处理 OperationCanceledException 。 有关详细信息,请参阅 任务取消。
通过轮询进行侦听
对于运行时间较长的循环或递归计算,可以通过定期轮询CancellationToken.IsCancellationRequested属性的值来侦听取消请求。 如果值为, true
则该方法应尽快清理并终止。 轮询的最佳频率取决于应用程序类型。 由开发人员决定任何给定程序的最佳轮询频率。 轮询本身不会显著影响性能。 以下示例演示了轮询的一种可能方法。
static void NestedLoops(Rectangle rect, CancellationToken token)
{
for (int col = 0; col < rect.columns && !token.IsCancellationRequested; col++) {
// Assume that we know that the inner loop is very fast.
// Therefore, polling once per column in the outer loop condition
// is sufficient.
for (int row = 0; row < rect.rows; row++) {
// Simulating work.
Thread.SpinWait(5_000);
Console.Write("{0},{1} ", col, row);
}
}
if (token.IsCancellationRequested) {
// Cleanup or undo here if necessary...
Console.WriteLine("\r\nOperation canceled");
Console.WriteLine("Press any key to exit.");
// If using Task:
// token.ThrowIfCancellationRequested();
}
}
Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)
Dim col As Integer
For col = 0 To rect.columns - 1
' Assume that we know that the inner loop is very fast.
' Therefore, polling once per column in the outer loop condition
' is sufficient.
For row As Integer = 0 To rect.rows - 1
' Simulating work.
Thread.SpinWait(5000)
Console.Write("0',1' ", col, row)
Next
Next
If token.IsCancellationRequested = True Then
' Cleanup or undo here if necessary...
Console.WriteLine(vbCrLf + "Operation canceled")
Console.WriteLine("Press any key to exit.")
' If using Task:
' token.ThrowIfCancellationRequested()
End If
End Sub
有关更完整的示例,请参见如何:通过轮询侦听取消请求。
通过注册回调进行侦听
某些操作可能会受到阻止,以致不能及时地检查取消标记的值。 对于这些情况,可以注册一个回调方法,该方法在收到取消请求时解锁方法。
该方法 Register 返回一个 CancellationTokenRegistration 专门用于此目的的对象。 以下示例演示如何使用 Register 该方法取消异步 Web 请求。
using System;
using System.Net.Http;
using System.Threading;
class Example4
{
static void Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
StartWebRequest(cts.Token);
// Cancellation will cause the web
// request to be cancelled.
cts.Cancel();
}
static void StartWebRequest(CancellationToken token)
{
var client = new HttpClient();
token.Register(() =>
{
client.CancelPendingRequests();
Console.WriteLine("Request cancelled!");
});
Console.WriteLine("Starting request.");
client.GetStringAsync(new Uri("http://www.contoso.com"));
}
}
Imports System.Net
Imports System.Net.Http
Imports System.Threading
Class Example4
Private Shared Sub Main4()
Dim cts As New CancellationTokenSource()
StartWebRequest(cts.Token)
' cancellation will cause the web
' request to be cancelled
cts.Cancel()
End Sub
Private Shared Sub StartWebRequest(token As CancellationToken)
Dim client As New HttpClient()
token.Register(Sub()
client.CancelPendingRequests()
Console.WriteLine("Request cancelled!")
End Sub)
Console.WriteLine("Starting request.")
client.GetStringAsync(New Uri("http://www.contoso.com"))
End Sub
End Class
该 CancellationTokenRegistration 对象管理线程同步,并确保回调在精确的时间点停止执行。
为了确保系统响应和避免死锁,注册回调时必须遵循以下准则:
回调方法应该快速,因为它进行同步调用,所以对 Cancel 的调用直到回调返回后才会返回。
如果回调正在运行时调用 Dispose 且你持有回调正在等待的锁定,则程序可能出现死锁。
Dispose
返回后,可以释放回调所需的任何资源。回调不应在回调中执行任何手动线程或 SynchronizationContext 使用情况。 如果回调必须在特定线程上运行,请使用 System.Threading.CancellationTokenRegistration 构造函数,该构造函数允许你指定目标“syncContext”为活跃的 SynchronizationContext.Current。 在回调中执行手动线程处理可能导致死锁。
要查看更完整的示例,请参阅 如何:注册取消请求的回调函数。
通过使用等待句柄进行侦听
当一个可取消的操作在等待同步基元(例如System.Threading.ManualResetEvent或System.Threading.Semaphore)时可能会阻塞,您可以使用CancellationToken.WaitHandle属性使该操作能够同时等待事件和取消请求。 取消令牌的等待句柄将在响应取消请求时发出信号,并且该方法可以使用 WaitAny 方法的返回值来确定是否由取消令牌发出信号。 然后,操作可以直接退出,或者根据需要抛出 OperationCanceledException。
// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
new TimeSpan(0, 0, 20));
' Wait on the event if it is not signaled.
Dim waitHandles() As WaitHandle = {mre, token.WaitHandle}
Dim eventThatSignaledIndex =
WaitHandle.WaitAny(waitHandles, _
New TimeSpan(0, 0, 20))
System.Threading.ManualResetEventSlim 并且 System.Threading.SemaphoreSlim 两者都支持其 Wait
方法中的取消框架。 可以将 CancellationToken 传递给该方法,在请求取消时,事件会被唤醒并引发 OperationCanceledException。
try
{
// mres is a ManualResetEventSlim
mres.Wait(token);
}
catch (OperationCanceledException)
{
// Throw immediately to be responsive. The
// alternative is to do one more item of work,
// and throw on next iteration, because
// IsCancellationRequested will be true.
Console.WriteLine("The wait operation was canceled.");
throw;
}
Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);
Try
' mres is a ManualResetEventSlim
mres.Wait(token)
Catch e As OperationCanceledException
' Throw immediately to be responsive. The
' alternative is to do one more item of work,
' and throw on next iteration, because
' IsCancellationRequested will be true.
Console.WriteLine("Canceled while waiting.")
Throw
End Try
' Simulating work.
Console.Write("Working...")
Thread.SpinWait(500000)
有关更完整的示例,请参见如何:侦听具有等待句柄的取消请求。
同时侦听多个标记
在某些情况下,监听器可能必须同时监听多个取消标记。 例如,除了在外部作为自变量传递到方法参数的标记以外,可取消操纵可能还必须监视内部取消标记。 为此,请创建一个链接令牌源,该源可将两个或多个令牌联接到一个令牌中,如以下示例所示。
public void DoWork(CancellationToken externalToken)
{
// Create a new token that combines the internal and external tokens.
this.internalToken = internalTokenSource.Token;
this.externalToken = externalToken;
using (CancellationTokenSource linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
{
try
{
DoWorkInternal(linkedCts.Token);
}
catch (OperationCanceledException)
{
if (internalToken.IsCancellationRequested)
{
Console.WriteLine("Operation timed out.");
}
else if (externalToken.IsCancellationRequested)
{
Console.WriteLine("Cancelling per user request.");
externalToken.ThrowIfCancellationRequested();
}
}
}
}
Public Sub DoWork(ByVal externalToken As CancellationToken)
' Create a new token that combines the internal and external tokens.
Dim internalToken As CancellationToken = internalTokenSource.Token
Dim linkedCts As CancellationTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken)
Using (linkedCts)
Try
DoWorkInternal(linkedCts.Token)
Catch e As OperationCanceledException
If e.CancellationToken = internalToken Then
Console.WriteLine("Operation timed out.")
ElseIf e.CancellationToken = externalToken Then
Console.WriteLine("Canceled by external token.")
externalToken.ThrowIfCancellationRequested()
End If
End Try
End Using
End Sub
请注意,完成后必须在链接标记源上调用 Dispose
。 有关更完整的示例,请参见如何:侦听多个取消请求。
库代码与用户代码之间的合作
统一的取消框架使库代码可以取消用户代码,以及用户代码以协作方式取消库代码。 顺利合作取决于双方遵循以下准则:
如果库代码提供可取消作,它还应提供接受外部取消令牌的公共方法,以便用户代码可以请求取消。
如果库代码调用用户代码,库代码应将 OperationCanceledException(externalToken)解释为 协作取消,不一定是失败异常。
用户代理应及时尝试响应来自库代码的取消请求。
System.Threading.Tasks.Task 和 System.Linq.ParallelEnumerable 是遵循这些准则的类的示例。 有关详细信息,请参阅 任务取消 和 作方法:取消 PLINQ 查询。