取消

.NET Framework 4 版为协作取消异步操作或长期运行的同步操作引入了新的统一模型。 此模型以一种名为“取消标记”的轻型对象为基础。 调用可取消操作(例如通过新建线程或任务)的对象会将此标记传递给该操作。 该操作接下来可将此标记的副本传递给其他操作。 以后,创建此标记的对象可以使用它请求该操作停止正在进行的任务。 只有请求对象可以发出取消请求,每个侦听器负责识别请求并及时对请求做出响应。 下图显示了标记源与其标记的所有副本之间的关系。

CancellationTokenSource 和 CancellationTokens

新的取消模型更便于创建可识别取消的应用程序和库,它支持下列功能:

  • 取消需要协作才能完成,不会对侦听器强制执行。 侦听器确定如何适当地终止操作来响应取消请求。

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

  • 请求对象只使用一个方法调用就能向所有标记副本发出取消请求。

  • 侦听器可以通过将多个标记连接成一个链接的标记来同时侦听这些标记。

  • 用户代码可以识别来自库代码的取消请求并对请求做出响应,库代码也可以识别来自用户代码的取消请求并对请求做出响应。

  • 可以通过轮询、回调注册或等待句柄,向侦听器通知取消请求。

新的取消类型

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

类型名称

说明

CancellationTokenSource

一个对象,它创建取消标记,并对标记的所有副本发出取消请求。

CancellationToken

传递给一个或多个侦听器的轻型值类型,通常作为方法参数传递。 侦听器通过轮询、回调或等待句柄监视标记的 IsCancellationRequested 属性值。

OperationCanceledException

此异常的新重载接受 CancellationToken 作为输入参数。 侦听器可以选择引发此异常,以验证取消源和通知其他代码它已响应取消请求。

新的取消模型以多种类型集成到 .NET Framework 中。最重要的类型包括 System.Threading.Tasks.ParallelSystem.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> 和 System.Linq.ParallelEnumerable。 建议将此新的取消模型用于所有新库和应用程序代码。

代码示例

在下面的示例中,请求对象创建一个 CancellationTokenSource 对象,然后将其 Token 属性传递给可取消操作。 接收请求的操作通过轮询监视标记的 IsCancellationRequested 属性值。 当值变为 true 时,侦听器可以通过任何适当的方式终止操作。 在此示例中,方法只是退出,在许多情况下只要求做到这一点即可。

注意注意

该示例使用 QueueUserWorkItem 方法演示新的取消框架与旧版 API 兼容。有关使用新的首选 System.Threading.Tasks.Task 类型的示例,请参见如何:取消任务及其子级

Shared Sub CancelWithThreadPoolMiniSnippet()


    'Thread 1: The Requestor
    ' Create the token source.
    Dim cts As New CancellationTokenSource()

    ' Pass the token to the cancelable operation.
    ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)

    ' Request cancellation by setting a flag on the token.
    cts.Cancel()
    ' end block
End Sub

'Thread 2: The Listener
Shared Sub DoSomeWork(ByVal obj As Object)

    Dim token As CancellationToken = CType(obj, CancellationToken)
    For i As Integer = 0 To 1000000

        ' Simulating work.
        Thread.SpinWait(5000000)

        If token.IsCancellationRequested Then

            ' Perform cleanup if necessary.
            '...
            ' Terminate the operation.
            Exit For
        End If
    Next
End Sub
static void CancelWithThreadPoolMiniSnippet()
{

    //Thread 1: The Requestor
    // Create the token source.
    CancellationTokenSource cts = new CancellationTokenSource();

    // Pass the token to the cancelable operation.
    ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);

    // Request cancellation by setting a flag on the token.
    cts.Cancel();
}

//Thread 2: The Listener
static void DoSomeWork(object obj)
{
    CancellationToken token = (CancellationToken)obj;
    for (int i = 0; i < 100000; i++)
    {
        // Simulating work.
        Thread.SpinWait(5000000);

        if (token.IsCancellationRequested)
        {
            // Perform cleanup if necessary.
            //...
            // Terminate the operation.
            break;
        }
    }
}

操作取消与对象取消

在新的取消框架中,取消是指操作取消,而不是对象取消。 取消请求指的是在执行任何所需的清理之后,应尽可能快地停止操作。 一个取消标记指的是一个“可取消操作”,但是可以在程序中实现该操作。 标记的 IsCancellationRequested 属性设置为 true 后,不能再重置为 false。 因此,“取消标记”在取消后不能再重新使用。

如果需要对象取消机制,可以将操作取消机制作为它的基础,如下面的示例所示。

Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token

' User defined Class with its own method for cancellation
Dim obj1 As New MyCancelableObject()
Dim obj2 As New MyCancelableObject()
Dim obj3 As New MyCancelableObject()

' 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()
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

// User defined Class with its own method for cancellation
var obj1 = new MyCancelableObject();
var obj2 = new MyCancelableObject();
var obj3 = new MyCancelableObject();

// 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();

如果对象支持多个并发可取消操作,则将单独的标记作为输入传递给每个不同的可取消操作。 这样,可以取消一个操作,而不会影响其他操作。

侦听和响应取消请求

在用户委托中,可取消操作的实施者确定如何为响应取消请求而终止操作。 在许多情况下,用户委托只需执行任何所需的清理,然后立即返回。

但是,在较复杂的情况下,用户委托可能需要通知库代码已执行取消操作。 在这种情况下,终止操作的正确方式是让委托调用 ThrowIfCancellationRequested(),这将导致引发 OperationCanceledException。 .NET Framework 4 版中此异常的新重载采用 CancellationToken 作为参数。 库代码可以在用户委托线程中捕获此异常,检查异常的标记以确定异常指示的是协作性取消还是其他一些异常情况。

Task 类按照这种方式处理 OperationCanceledException。 有关更多信息,请参见任务取消

通过轮询侦听

对于循环或递归的长时间运行的计算,可以通过定期轮询 CancellationToken.IsCancellationRequested 属性值来侦听取消请求。 如果该值为 true,则方法应尽快清理并终止。 最佳轮询频率取决于应用程序的类型。 开发人员应确定任何给定程序的最佳轮询频率。 轮询本身并不会显著影响性能。 下面的示例演示一种可能的轮询方式。

    Shared Sub NestedLoops(ByVal rect As Rectangle, ByVal token As CancellationToken)

        For x As Integer = 0 To rect.columns

            For y As Integer = 0 To rect.rows

                ' Simulating work.
                Thread.SpinWait(5000)
                Console.Write("0' end block,1' end block ", x, y)
            Next

            ' Assume that we know that the inner loop is very fast.
            ' Therefore, checking once per row is sufficient.
            If token.IsCancellationRequested = True Then

                ' Cleanup or undo here if necessary...
                Console.WriteLine("\r\nCancelling after row 0' end block.", x)
                Console.WriteLine("Press any key to exit.")
                ' then...
                Exit For
                ' ...or, if using Task:
                ' token.ThrowIfCancellationRequested()
            End If
        Next
    End Sub

static void NestedLoops(Rectangle rect, CancellationToken token)
{
    for (int x = 0; x < rect.columns && !token.IsCancellationRequested; x++)
    {
        for (int y = 0; y < rect.rows; y++)
        {
            // Simulating work.
            Thread.SpinWait(5000);
            Console.Write("{0},{1} ", x, y);
        }

        // Assume that we know that the inner loop is very fast.
        // Therefore, checking once per row is sufficient.
        if (token.IsCancellationRequested)
        {
            // Cleanup or undo here if necessary...
            Console.WriteLine("\r\nCancelling after row {0}.", x);
            Console.WriteLine("Press any key to exit.");
            // then...
            break;
            // ...or, if using Task:
            // token.ThrowIfCancellationRequested();
        }
    }
}

有关更完整的示例,请参见 如何:通过轮询侦听取消请求

通过注册回调进行侦听

某些操作可能被阻止,因此无法及时检查取消标记的值。 对于这些情况,可以注册一个回调方法,以便在收到取消请求后取消阻止该方法。

Register 方法会返回一个专用于此目的的 CancellationTokenRegistration 对象。 下面的示例演示如何使用 Register 方法取消异步 Web 请求。

Dim cts As New CancellationTokenSource()
Dim token As CancellationToken = cts.Token
Dim wc As New WebClient()

' To request cancellation on the token
' will call CancelAsync on the WebClient.
token.Register(Sub() wc.CancelAsync())

Console.WriteLine("Starting request")
wc.DownloadStringAsync(New Uri("https://www.contoso.com"))
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;            
WebClient wc = new WebClient();

// To request cancellation on the token
// will call CancelAsync on the WebClient.
token.Register(() => wc.CancelAsync());

Console.WriteLine("Starting request");
wc.DownloadStringAsync(new Uri("https://www.contoso.com"));

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.
Dim myWaitHandle(2) As WaitHandle
myWaitHandle(0) = mre
myWaitHandle(1) = token.WaitHandle
Dim eventThatSignaledIndex =
    WaitHandle.WaitAny(myWaitHandle, _
                        New TimeSpan(0, 0, 20))
// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
    WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                        new TimeSpan(0, 0, 20));

在面向 .NET Framework 4 版的新代码中,System.Threading.ManualResetEventSlimSystem.Threading.SemaphoreSlim 都在其 Wait 方法中支持新取消框架。 您可以将 CancellationToken 传递给该方法,在请求取消时,该事件会唤醒,并引发 OperationCanceledException

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)
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 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
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。 有关更完整的示例,请参见 如何:侦听多个取消请求

库代码和用户代码之间的协作

统一取消框架使得库代码和用户代码可以以协作的方式互相取消对方。 协作顺畅与否取决于每一方是否遵循以下规则:

  • 如果库代码提供可取消操作,它还应当提供接受外部取消标记的公共方法,以便用户代码可以请求取消。

  • 如果库代码调用用户代码,则库代码应当将 OperationCanceledException(externalToken) 解释为协作性取消,而不一定要解释为失败异常。

  • 用户委托应当尝试及时响应来自库代码的取消请求。

System.Threading.Tasks.TaskSystem.Linq.ParallelEnumerable 是遵循这些规则的类的示例。 有关更多信息,请参见任务取消如何:取消 PLINQ 查询

请参见

其他资源

托管线程处理基本知识