托管线程中的取消

从 .NET Framework 4 开始,.NET 在协作式取消异步操作或长时间运行的同步操作时使用统一的模型。 此模型基于被称为取消标记的轻量对象。 调用一个或多个可取消操作的对象(例如通过创建新线程或任务)将标记传递给每个操作。 单个操作反过来可将标记的副本传递给其他操作。 稍后,创建标记的对象可使用此标记请求停止执行操作内容。 只有发出请求的对象,才能发出取消请求,而每个侦听器负责侦听是否有请求,并及时适当地响应请求。

用于实现协作取消模型的常规模式是:

重要

CancellationTokenSource 类实现 IDisposable 接口。 使用取消标记源释放所包含的任何非托管资源后,应确保调用 CancellationTokenSource.Dispose 方法。

下图显示了标记源与标记的所有副本之间的关系。

CancellationTokenSource and cancellation tokens

借助协作式取消模型,可更轻松地创建取消感知应用程序和库,并且该模型支持以下功能:

  • 取消具有协作性,且不会在侦听器上强制执行。 侦听器确定如何适当地以响应取消请求终止操作。

  • 请求与侦听不同。 调用可取消操作的对象可以控制何时(如果有)请求取消。

  • 请求对象仅使用一种方法调用,向标记的所有副本发出取消请求。

  • 侦听器可以将多个令牌联接到一个链接令牌,从而同时侦听多个令牌。

  • 用户代码可以注意并响应来自库代码的取消请求,而库代码可以注意并响应来自用户代码的取消请求。

  • 侦听器可通过轮询、回调注册或等待等待句柄来接收到取消请求的通知。

取消类型

取消框架作为相关类型集实现,如下表所列。

类型名称 描述
CancellationTokenSource 创建取消标记并为此标记的所有副本发出取消请求的对象。
CancellationToken 通常作为方法参数传递给一个或多个侦听器的轻量值类型。 侦听器通过轮询、回调或等待句柄监视标记的 IsCancellationRequested 属性的值。
OperationCanceledException 此异常的构造函数的重载将 CancellationToken 作为参数接受。 侦听器可能会选择性地引发此异常,议验证取消源并通知其他侦听器它已响应取消请求。

取消模型以多种类型集成到 .NET 中。 最重要的类型包括 System.Threading.Tasks.ParallelSystem.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 Example
    Public Sub Main()
        ' 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 {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
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 Example
    Public Sub Main()
        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 col As Integer = 0 To rect.rows - 1
            ' Simulating work.
            Thread.SpinWait(5000)
            Console.Write("0',1' ", x, y)
        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;
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"));
    }
}
Imports System.Net
Imports System.Threading

Class Example
    Private Shared Sub Main()
        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 wc As New WebClient()
        wc.DownloadStringCompleted += Function(s, e) Console.WriteLine("Request completed.")

        ' Cancellation on the token will 
        ' call CancelAsync on the WebClient.
        token.Register(Function()
                           wc.CancelAsync()
                           Console.WriteLine("Request cancelled!")

                       End Function)

        Console.WriteLine("Starting request.")
        wc.DownloadStringAsync(New Uri("http://www.contoso.com"))
    End Sub
End Class

CancellationTokenRegistration 对象管理线程同步,并确保回调将在精确的时间点停止执行。

为了确保系统的响应能力并避免死锁,注册回调时必须遵循以下准则:

  • 回调方法应该快速,因为它进行同步调用,所以对 Cancel 的调用直到回调返回后才会返回。

  • 如果回调正在运行时调用 Dispose 且你持有回调正在等待的锁定,则程序可能出现死锁。 Dispose 返回后,可释放回调所需的任何资源。

  • 回调不应在回调中执行任何手动线程或 SynchronizationContext 使用情况。 如果回调必须在特定线程上运行,请使用 System.Threading.CancellationTokenRegistration 构造函数,此函数使你能够指定目标 syncContext 是活动的 SynchronizationContext.Current。 在回调中执行手动线程处理可能导致死锁。

有关更完整的示例,请参见如何:注册取消请求的回叫

通过使用等待句柄进行侦听

当可取消的操作可在等待同步基元(例如 System.Threading.ManualResetEventSystem.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.ManualResetEventSlimSystem.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.TaskSystem.Linq.ParallelEnumerable 是遵循这些准则的类的示例。 有关详细信息,请参阅任务取消如何:取消 PLINQ 查询

请参阅