从 .NET Framework 4 开始,.NET 在协作式取消异步操作或长时间运行的同步操作时使用统一的模型。 此模型基于被称为取消标记的轻量对象。 调用一个或多个可取消操作的对象(例如通过创建新线程或任务)将标记传递给每个操作。 单个操作反过来可将标记的副本传递给其他操作。 稍后,创建标记的对象可使用此标记请求停止执行操作内容。 只有发出请求的对象,才能发出取消请求,而每个侦听器负责侦听是否有请求,并及时适当地响应请求。
用于实现协作取消模型的常规模式是:
-
实例化
CancellationTokenSource
对象,此对象管理取消通知并将其发送给单个取消标记。 -
将
CancellationTokenSource.Token
属性返回的标记传递给每个侦听取消的任务或线程。 -
为每个任务或线程提供响应取消的机制。
-
调用
CancellationTokenSource.Cancel
方法以提供取消通知。
备注 CancellationTokenSource 类实现 IDisposable 接口。 使用取消标记源释放所包含的任何非托管资源后,应确保调用 CancellationTokenSource.Dispose 方法。
下图显示了标记源与标记的所有副本之间的关系。
借助协作式取消模型,可更轻松地创建取消感知应用程序和库,并且该模型支持以下功能:
-
取消具有协作性,且不会在侦听器上强制执行。 侦听器确定如何适当地以响应取消请求终止操作。
-
请求与侦听不同。 调用可取消操作的对象可以控制何时(如果有)请求取消。
-
请求对象仅使用一种方法调用,向标记的所有副本发出取消请求。
-
侦听器可以将多个令牌联接到一个链接令牌,从而同时侦听多个令牌。
-
用户代码可以注意并响应来自库代码的取消请求,而库代码可以注意并响应来自用户代码的取消请求。
-
侦听器可通过轮询、回调注册或等待等待句柄来接收到取消请求的通知。
取消类型
取消框架作为相关类型集实现,如下表所列。
类型名称 | 描述 |
---|---|
CancellationTokenSource | 创建取消标记并为此标记的所有副本发出取消请求的对象。 |
CancellationToken | 通常作为方法参数传递给一个或多个侦听器的轻量值类型。 侦听器通过轮询、回调或等待句柄监视标记的 IsCancellationRequested 属性的值。 |
OperationCanceledException | 此异常的构造函数的重载将 CancellationToken 作为参数接受。 侦听器可能会选择性地引发此异常,议验证取消源并通知其他侦听器它已响应取消请求。 |
取消模型以多种类型集成到 .NET 中。 最重要的类型包括
System.Threading
.Tasks.Parallel
、System.Threading.Tasks.Task、System.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...
操作取消与对象取消
在协作式取消框架中,取消将引用操作,而不是对象。 取消请求意味着应在执行任何所需的清理后尽快停止操作。 一个取消标记应代指一个“可取消操作”,但可在程序中实现此操作。 在标记的 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 {0} Cancel callback", id); // 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
如果对象支持多个并发可取消操作,则将单独的标记作为输入传递给每个非重复的可取消操作。 这样,无需影响其他操作即可取消某项操作。
侦听和响应取消请求
在用户委托中,可取消操作的实施者确定如何以响应取消请求来终止操作。 在很多情况下,用户委托只需执行全部所需清理,然后立即返回。
但是,在更复杂的情况下,用户委托可能需要通知库代码已发生取消。 在这种情况下,终止操作的正确方式是委托调用 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(); } }
通过注册回调进行侦听
某些操作可能被阻止,导致其无法及时检查取消标记的值。 对于这些情况,可以注册在接收取消请求时取消阻止此方法的回调方法。
Register
方法返回专用于此目的的 CancellationTokenRegistration
对象。
以下示例演示了如何使用 Register 方法取消异步 Web 请求。
using System; using System.Net; 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) { WebClient wc = new WebClient(); wc.DownloadStringCompleted += (s, e) => Console.WriteLine("Request completed."); // Cancellation on the token will // call CancelAsync on the WebClient. token.Register(() => { wc.CancelAsync(); Console.WriteLine("Request cancelled!"); }); Console.WriteLine("Starting request."); wc.DownloadStringAsync(new Uri("http://www.contoso.com")); } }
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));
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);
同时侦听多个标记
在某些情况下,侦听器可能需要同时侦听多个取消标记。 例如,除了在外部作为自变量传递到方法参数的标记以外,可取消操纵可能还必须监视内部取消标记。 为此,需创建可将两个或多个标记联接成一个标记的链接标记源,如以下示例所示。
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(); } } } }
请注意,完成后必须在链接标记源上调用 Dispose。 有关更完整的示例,请参见如何:侦听多个取消请求。