共用方式為


受管理的執行緒中的取消

從 .NET Framework 4 開始,.NET 會使用統一模型來合作取消異步或長時間執行的同步作業。 此模型是以稱為取消標記的輕量型物件為基礎。 叫用一或多個可取消作業的物件,例如透過建立新線程或任務,將令牌傳遞給每個作業。 個別作業接著可以將令牌的複本傳遞至其他作業。 稍後,建立令牌的物件可以使用它來要求作業停止正在進行的活動。 只有發出要求的物件可以取消請求,且每個監聽器負責注意並以適當且及時的方式回應此請求。

實作合作式取消模型的一般模式為:

這很重要

CancellationTokenSource 類別會實作 IDisposable 介面。 當您完成使用取消令牌來源時,請務必呼叫 CancellationTokenSource.Dispose 方法,以釋放它保留的任何非受控資源。

下圖顯示令牌來源與其所有令牌複本之間的關聯性。

CancellationTokenSource 和取消令牌

合作式取消模型可讓您更輕鬆地建立取消感知應用程式和連結庫,並支援下列功能:

  • 取消是合作的,而且不會強加給聽者。 接聽程式決定如何正常終止以回應取消要求。

  • 要求與接聽不同。 叫用可取消作業的物件可以控制何時(如果有的話)取消要求。

  • 要求物件只需使用一個方法呼叫,即可對令牌的所有複本提出取消請求。

  • 接聽程式可以同時接聽多個令牌,方法是將它們聯結至一個 連結的令牌

  • 用戶程式代碼可以注意到並回應連結庫程序代碼的取消要求,而連結庫程式代碼可以注意到並響應使用者程式代碼的取消要求。

  • 監聽者可藉由輪詢、回呼註冊或等待處理來收到取消要求的通知。

取消類型

取消架構會實作為一組相關的類型,如下表所列。

類型名稱 說明
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

如需更完整的範例,請參閱 如何:透過輪詢接聽取消要求

通過註冊回呼來監聽

某些操作可能會被封鎖,以至於無法及時檢查取消令牌的值。 在這些情況下,您可以註冊回呼方法,以在收到取消要求時解除封鎖方法。

方法 RegisterCancellationTokenRegistration 傳回特別用於此目的的物件。 下列範例示範如何使用 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.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 查詢

另請參閱