取消
.NET Framework 4 版 新加入了統一的非同步或長時間同步作業的合作式取消模型。 此模型架構在稱為取消語彙基元的輕量型物件上。 叫用可取消作業 (例如,透過建立新執行緒或工作) 的物件會將語彙基元傳遞給作業, 該作業又會將語彙基元複本傳遞給其他作業。 而在稍後,建立語彙基元的物件可以利用該語彙基元要求作業停止目前執行的工作。 只有提出要求的物件可以發行取消要求,每個接聽程式都必須留意該要求並且即時回應要求。 下圖顯示語彙基元來源與其所有複本之間的關係。
新的取消模型簡化了取消感知 (Cancellation-Aware) 應用程式與程式庫的建立程序,並且支援下列功能:
取消屬於合作性作業,而且不會在接聽程式上強制執行。 接聽程式能夠決定要如何順利終止來回應取消要求。
提出要求與接聽是不同的動作。 叫用可取消作業的物件可以控制提出取消要求的時機 (如有需要)。
提出要求的物件只使用一個方法呼叫,就能發行取消要求給所有語彙基元複本。
藉著將所有語彙基元加入到同一個「連結的語彙基元」(Linked Token) 中,接聽程式可以同時接聽多個語彙基元。
使用者程式碼可以注意並回應來自程式庫程式碼的取消要求,而程式庫程式碼可以注意並回應來自使用者程式碼的取消要求。
接聽程式可以透過輪詢、回呼登錄或等待等候控制代碼收到取消要求通知。
新的取消型別
新的取消架構會實作為一組相關的型別,如下表所示:
型別名稱 |
說明 |
---|---|
物件,會建立取消語彙基元並針對該語彙基元的所有複本發行取消要求。 |
|
傳遞給一個或多個接聽程式的輕量實值型別,通常做為方法參數。 接聽程式會透過輪詢、回呼或者等候控制代碼監視語彙基元之 IsCancellationRequested 屬性的值。 |
|
這個例外狀況的新多載接受 CancellationToken 做為輸入參數。 接聽程式可以選擇性地擲回這個例外狀況,以確認取消的來源並且通知其他接聽程式它已經回應取消要求。 |
新的取消模型會整合至 .NET Framework 的數種型別中。其中最重要的是 System.Threading.Tasks.Parallel、System.Threading.Tasks.Task、System.Threading.Tasks.Task<TResult> 和 System.Linq.ParallelEnumerable。 在此建議您針對所有新程式庫與應用程式程式碼,都使用這個新的取消模型。
程式碼範例
在下列範例中,提出要求的物件會建立 CancellationTokenSource 物件,並接著將它的 Token 屬性傳遞給取消作業。 接受要求的作業會透過輪詢來監視 IsCancellationRequested 屬性的值。 當值變為 true 時,接聽程式能夠以任何可能的行為適時終止。 在這個範例中,方法只會結束,這在許多情況中都要必要的。
注意事項 |
---|
範例中使用 QueueUserWorkItem 方法示範新的取消架構與舊有 API 相容。如需使用這項全新且偏好 System.Threading.Tasks.Task 型別的範例,請參閱 HOW TO:取消工作及其子系。 |
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();
}
}
}
如需更完整的範例,請參閱 HOW TO:透過輪詢接聽取消要求。
藉由註冊回呼來接聽
某些作業可能會因為無法即時檢查取消語彙基元的值而變成封鎖狀態。 對於這種案例,您可以註冊回呼方法,在收到取消要求時解除封鎖方法。
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。 在回呼中執行手動執行緒,可能會造成死結。
如需更完整的範例,請參閱 HOW TO:註冊取消要求的回呼。
藉由使用等候控制代碼來接聽
如果可取消作業可能在等候同步原始物件 (例如 System.Threading.ManualResetEvent 或 System.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.ManualResetEventSlim 與 System.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);
如需更完整的範例,請參閱 HOW TO:接聽具有等候控制代碼的取消要求。
同時接聽多個語彙基元
在某些情況中,接聽程式必須同時接聽多個取消語彙基元。 例如,除了外部傳入做為方法參數之引數的語彙基元,可取消的作業可能還必須監視內部取消語彙基元。 若要完成這項工作,請建立可以將兩個 (含) 以上語彙基元加入一個語彙基元中的連結語彙基元來源,如下範例所示:
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。 如需更完整的範例,請參閱 HOW TO:接聽多個取消要求。
程式庫程式碼與使用者程式碼之間的合作
統一取消架構可以透過合作的方式,讓程式庫程式碼取消使用者程式碼,並且讓使用者程式碼取消程式庫程式碼。 兩者都必須遵守下列原則,才能合作無間:
如果程式庫程式碼提供可取消的作業,則也應該提供接受外部取消語彙基元的公用方法,讓使用者程式碼可以要求取消。
如果程式庫程式碼呼叫使用者程式碼內部,前者應該將 OperationCanceledException(externalToken) 解譯為「合作性取消」(Cooperative Cancellation),不一定要解譯為失敗例外狀況。
使用者委派應該嘗試即時回應來自程式庫程式碼的取消要求。
System.Threading.Tasks.Task 與 System.Linq.ParallelEnumerable 便是遵循這些原則的類別範例。 如需詳細資訊,請參閱 工作取消 和 HOW TO:取消 PLINQ 查詢。