托管线程中的取消

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

实现协作取消模型的一般模式是:

重要

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

下图显示了令牌源与其令牌的所有副本之间的关系。

CancellationTokenSource 和 cancellation 令牌

借助协作取消模型,可以更轻松地创建可识别取消的应用程序和库,并支持以下功能:

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

  • 请求不同于倾听。 调用可取消操作的对象可以控制何时(是否会请求)取消。

  • 请求对象通过一次方法调用向令牌的所有副本发送取消请求。

  • 侦听器可以通过将它们联接到一个 链接令牌中来同时侦听多个令牌。

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

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

取消类型

取消框架作为一组相关类型实现,下表中列出了这些类型。

类型名称 DESCRIPTION
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 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.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.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.TaskSystem.Linq.ParallelEnumerable 是遵循这些准则的类的示例。 有关详细信息,请参阅 任务取消作方法:取消 PLINQ 查询

另请参阅