從 .NET Framework 4 開始,.NET 會使用統一模型來合作取消異步或長時間執行的同步作業。 此模型是以稱為取消標記的輕量型物件為基礎。 叫用一或多個可取消作業的物件,例如透過建立新線程或任務,將令牌傳遞給每個作業。 個別作業接著可以將令牌的複本傳遞至其他作業。 稍後,建立令牌的物件可以使用它來要求作業停止正在進行的活動。 只有發出要求的物件可以取消請求,且每個監聽器負責注意並以適當且及時的方式回應此請求。
實作合作式取消模型的一般模式為:
具現化 CancellationTokenSource 物件,該物件會管理並傳送取消通知給個別的取消令牌。
將CancellationTokenSource.Token屬性回傳的令牌傳遞給接聽取消的每個工作或執行緒。
為每個工作或線程提供回應取消的機制。
呼叫CancellationTokenSource.Cancel方法以便提供取消通知。
這很重要
CancellationTokenSource 類別會實作 IDisposable 介面。 當您完成使用取消令牌來源時,請務必呼叫 CancellationTokenSource.Dispose 方法,以釋放它保留的任何非受控資源。
下圖顯示令牌來源與其所有令牌複本之間的關聯性。
合作式取消模型可讓您更輕鬆地建立取消感知應用程式和連結庫,並支援下列功能:
取消是合作的,而且不會強加給聽者。 接聽程式決定如何正常終止以回應取消要求。
要求與接聽不同。 叫用可取消作業的物件可以控制何時(如果有的話)取消要求。
要求物件只需使用一個方法呼叫,即可對令牌的所有複本提出取消請求。
接聽程式可以同時接聽多個令牌,方法是將它們聯結至一個 連結的令牌。
用戶程式代碼可以注意到並回應連結庫程序代碼的取消要求,而連結庫程式代碼可以注意到並響應使用者程式代碼的取消要求。
監聽者可藉由輪詢、回呼註冊或等待處理來收到取消要求的通知。
取消類型
取消架構會實作為一組相關的類型,如下表所列。
類型名稱 | 說明 |
---|---|
CancellationTokenSource | 建立取消令牌的物件,也會針對該令牌的所有複本發出取消要求。 |
CancellationToken | 傳遞至一或多個監聽器的輕量型值型別,通常是作為方法參數。 監聽器透過輪詢、回呼或等候句柄來監視IsCancellationRequested 令牌的屬性值。 |
OperationCanceledException | 這個例外狀況建構函式的多載接受 CancellationToken 做為參數。 接聽程式可以選擇性地擲回此例外狀況,以驗證取消的來源,並通知其他人已回應取消要求。 |
取消模型會整合到數種類型的 .NET 中。 最重要的是System.Threading.Tasks.Parallel、 System.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.ManualResetEvent 或 System.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.Task 和 System.Linq.ParallelEnumerable 是遵循這些指導方針的類別範例。 如需詳細資訊,請參閱 工作取消 和 如何:取消 PLINQ 查詢。