Managed 執行緒中的取消作業

從 .NET Framework 4 開始,.NET 在對非同步作業或長時間執行的同步作業進行合作式取消時,會使用統一的模型。 此模型是根據一個被稱為取消權杖的輕量級物件。 叫用一或多個可取消作業的物件,例如藉由建立新的執行緒或工作,會將權杖傳遞至每個作業。 個別作業可以依序將權杖的複本傳遞至其他作業。 之後的某些時候 ,建立權杖的物件可以使用它來要求作業停止活動。 只有要求的物件可以發出取消要求,而且每個接聽程式負責留意要求,並且以適當且即時的方式回應。

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

重要

CancellationTokenSource 類別會實作 IDisposable 介面。 當您完成使用取消權杖來釋放任何它所保留的 Unmanaged 資源之後,您一定要呼叫 CancellationTokenSource.Dispose 方法。

下圖顯示權杖來源和其權杖的所有複本兩者之間的關係。

CancellationTokenSource and cancellation tokens

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

  • 取消作業為合作式且對於接聽程式為非強制。 接聽程式會決定如何依正常程序來終止以回應取消要求。

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

  • 送出要求的物件藉著只使用一個方法呼叫,來發出取消要求給權杖的所有複本。

  • 接聽程式可以透過將這些權杖聯結成單一個「連結的權杖」(linked token) 來同時接聽多個權杖。

  • 使用者程式碼可以注意並回應來自程式庫程式碼的取消要求,並且程式庫程式碼可以注意並回應來自使用者程式碼的取消要求。

  • 接聽程式可以收到取消要求,藉由輪詢、回呼註冊,或等候等候控制代碼。

取消類型

取消架構被實作為一組相關的類型,這些會在下表中列出。

類型名稱 描述
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 方法來示範合作式取消架構與舊版應用程式開發介面相容。 如需使用慣用的 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

如果物件支援多個並行的可取消作業,會將個別的權杖做為輸入傳遞給每個不同的可取消作業。 如此一來,可以取消一項作業而不會對其他造成影響。

接聽並回應取消要求

在使用者委派,可取消作業的實作器會決定如何終止作業以取消要求的回應。 在許多情況下,使用者委派可以只執行任何必要的清除作業,然後立即傳回。

不過,在更複雜的情況下,使用者委派可能需要通知程式庫程式碼已發生取消。 在這種情況下,終止作業的正確方式是委派要呼叫會導致擲回 OperationCanceledExceptionThrowIfCancellationRequested 方法。 程式庫程式碼可以在使用者委派執行緒上攔截此例外狀況,並檢查例外狀況的權杖來判斷此例外狀況是否表示合作式取消或一些其他的例外狀況。

Task 類別以這種方式控制 OperationCanceledException 。 如需詳細資訊,請參閱 Task Cancellation

透過輪詢接聽

針對長時間執行計算的迴圈或遞迴,您可以透過定期輪詢 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 查詢

另請參閱