C#如何優(yōu)雅地取消進程的執(zhí)行之Cancellation詳解
概述
從.NET Framework 4開始,.NET使用統(tǒng)一的模型來協(xié)作取消異步或長時間運行的同步線程。該模型基于一個稱為CancellationToken的輕量級對象。這個對象在調(diào)用一個或多個取消線程時(例如通過創(chuàng)建新線程或任務(wù)),是通過將token傳遞給每個線程來完成的(通過鏈式的方式依次傳遞)。單個線程能夠依次地將token的副本傳遞給其他線程。
之后,在適當?shù)哪硞€時機,創(chuàng)建token的對象就可以使用token來請求線程停止。只有請求對象可以發(fā)出取消請求,每個監(jiān)聽器負責監(jiān)聽到請求并以適當和及時的方式響應(yīng)取消請求。
實現(xiàn)協(xié)作取消模型的一般模式是:
- 1、實例化一個CancellationTokenSource對象,該對象管理cancellation并將cancellation通知發(fā)送給單獨的cancellation token。
- 2、CancellationTokenSource對象的Token屬性,可以返回一個Token對象,我們可以將該Token對象發(fā)送給每個監(jiān)聽該cancellation的進程或Task。
- 3、為每個任務(wù)或線程提供響應(yīng)取消的機制。
- 4、調(diào)用 CancellationTokenSource.Cancel() 方法,來取消線程或者Task。
【tips】我們在使用cancellation的token取消線程后,應(yīng)該確保調(diào)用CancellationTokenSource.Dispose()方法,以便于釋放它持有的任何非托管資源。。
下圖展示出了CancellationTokenSource對象里的Token屬性對象,是如何傳遞到其他的線程里的。

合作取消模型使創(chuàng)建取消感知的應(yīng)用程序和庫變得更容易,它支持以下功能:
- 1、取消是合式的,不會強加給監(jiān)聽器。監(jiān)聽器確定如何優(yōu)雅地終止以響應(yīng)取消請求。
- 2、請求不同于監(jiān)聽。調(diào)用可取消的線程的對象,可以控制何時(如果有的話)取消被請求。
- 3、請求的對象,可以通過僅使用一個方法,即可發(fā)送取消請求到所有的token副本中。
- 4、監(jiān)聽器可以通過將多個Token連接成一個linked Token,來同時監(jiān)聽多個Token。
- 5、用戶代碼可以注意到并響應(yīng)library code的取消請求,而library code可以注意到并響應(yīng)用戶代碼的取消請求。
- 6、可以通過輪詢、回調(diào)注冊或等待等待句柄的方式,來通知監(jiān)聽器執(zhí)行取消請求。
與取消線程相關(guān)的類型
取消框架是作為一組相關(guān)類型實現(xiàn)的,這些類型在下表中列出。
| CancellationTokenSource | 該對象創(chuàng)建cancellation token,并向 cancellation token的所有副本分發(fā)取消請求。 |
| CancellationToken | 傳遞給一個或多個監(jiān)聽器的輕量級的值類型,通常作為方法參數(shù)。偵聽器通過輪詢、回調(diào)或等待句柄監(jiān)視token的IsCancellationRequested屬性的值。 |
| OperationCanceledException | 此異常構(gòu)造函數(shù)的重載,接受CancellationToken作為參數(shù)。偵聽器可以選擇性地拋出此異常以驗證取消的來源,并通知其他已響應(yīng)取消請求監(jiān)聽器。 |
取消模型以幾種類型集成到.net中。
最重要的是System.Threading.Tasks.Parallel,System.Threading.Tasks.Task、System.Threading.Tasks.Task<TResult> 和 System.Linq.ParallelEnumerable。
建議使用所有新的庫和應(yīng)用代碼來實現(xiàn)合作市取消模式。
代碼舉例
在下面的示例中,請求對象創(chuàng)建一個CancellationTokenSource對象,然后將該對象的Token屬性傳遞給可取消的進程。
接收請求的線程通過輪詢來監(jiān)視Token的IsCancellationRequested屬性的值。
當該值變?yōu)閠rue時,偵聽器可以以任何合適的方式終止。在本例中,方法只是退出,這是許多情況下所需要的全部內(nèi)容。
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...操作取消vs對象取消
在協(xié)作取消框架中,取消指的是操作(線程中執(zhí)行的操作),而不是對象。取消請求意味著在執(zhí)行任何所需的清理后,操作應(yīng)盡快停止。一個cancellation token應(yīng)該指向一個“可取消的操作”,無論該操作如何在您的程序中實現(xiàn)。
在token的IsCancellationRequested屬性被設(shè)置為true之后,它不能被重置為false。因此,取消令牌在被取消后不能被重用。
如果您需要對象取消機制,您可以通過調(diào)用CancellationToken來基于操作取消機制。注冊方法,如下例所示。
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如果一個對象支持多個并發(fā)的可取消操作,則可以給每個不同的可取消操作各自傳入一個不同的token。這樣,一個操作可以被取消而不會影響到其他操作。
監(jiān)聽并響應(yīng)取消請求
在用戶委托中,可取消操作的實現(xiàn)者決定如何終止該操作以響應(yīng)取消請求。在許多情況下,用戶委托可以只執(zhí)行任何所需的清理,然后立即返回。
但是,在更復(fù)雜的情況下,可能需要用戶委托通知庫代碼已發(fā)生cancellation。在這種情況下,終止操作的正確方法是委托調(diào)用ThrowIfCancellationRequested方法,這將導(dǎo)致拋出OperationCanceledException異常。庫代碼可以在用戶委托線程上捕獲此異常,并檢查異常的token,以確定該異常是否表示協(xié)作取消或其他異常情況。
在這種情況下,終止操作的正確方法是委托調(diào)用ThrowIfCancellationRequested方法,這將導(dǎo)致拋出OperationCanceledException。庫代碼可以在用戶委托線程上捕獲此異常,并檢查異常的token,以確定該異常是否表示協(xié)作取消或其他異常情況。
輪詢監(jiān)聽
對于循環(huán)或遞歸的長時間運行的計算,可以通過定期輪詢CancellationToken.IsCancellationRequested的值來監(jiān)聽取消請求。如果它的值為true,則該方法應(yīng)該盡快清理并終止。輪詢的最佳頻率取決于應(yīng)用程序的類型。開發(fā)人員可以為任何給定的程序確定最佳輪詢頻率。輪詢本身不會顯著影響性能。
下面的程序案例展示了一種可能的輪詢方式。
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();
}
}下面的程序代碼是一個詳細的實現(xiàn):
using System;
using System.Threading;
public class ServerClass
{
public static void StaticMethod(object obj)
{
CancellationToken ct = (CancellationToken)obj;
Console.WriteLine("ServerClass.StaticMethod is running on another thread.");
// Simulate work that can be canceled.
while (!ct.IsCancellationRequested) {
Thread.SpinWait(50000);
}
Console.WriteLine("The worker thread has been canceled. Press any key to exit.");
Console.ReadKey(true);
}
}
public class Simple
{
public static void Main()
{
// The Simple class controls access to the token source.
CancellationTokenSource cts = new CancellationTokenSource();
Console.WriteLine("Press 'C' to terminate the application...\n");
// Allow the UI thread to capture the token source, so that it
// can issue the cancel command.
Thread t1 = new Thread(() => { if (Console.ReadKey(true).KeyChar.ToString().ToUpperInvariant() == "C")
cts.Cancel(); } );
// ServerClass sees only the token, not the token source.
Thread t2 = new Thread(new ParameterizedThreadStart(ServerClass.StaticMethod));
// Start the UI thread.
t1.Start();
// Start the worker thread and pass it the token.
t2.Start(cts.Token);
t2.Join();
cts.Dispose();
}
}
// The example displays the following output:
// Press 'C' to terminate the application...
//
// ServerClass.StaticMethod is running on another thread.
// The worker thread has been canceled. Press any key to exit.通過回調(diào)注冊進行監(jiān)聽
以這種方式進行的某些操作可能會阻塞,從而無法及時檢查cancellation token的值。對于這些情況,您可以注冊一個回調(diào)方法,以便在收到取消請求時解除對該方法的阻塞。
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"));
}
}CancellationTokenRegistration對象管理線程同步,并確?;卣{(diào)將在精確的時間點停止執(zhí)行。
為了確保系統(tǒng)響應(yīng)性并避免死鎖,在注冊回調(diào)時必須遵循以下準則:
1、回調(diào)方法應(yīng)該是快速的,因為它是同步調(diào)用的,因此對Cancel的調(diào)用在回調(diào)返回之前不會返回。
2、如果在回調(diào)運行時調(diào)用Dispose,并且持有回調(diào)等待的鎖,則程序可能會死鎖。Dispose返回后,您可以釋放回調(diào)所需的任何資源。
3、Callbacks 不應(yīng)該執(zhí)行任何手動線程或在回調(diào)中使用SynchronizationContext。如果回調(diào)必須在特定線程上運行,則使用System.Threading.CancellationTokenRegistration構(gòu)造函數(shù),該構(gòu)造函數(shù)使您能夠指定目標syncContext是活動的SynchronizationContext.Current。在回調(diào)中執(zhí)行手動線程會導(dǎo)致死鎖。
使用WaitHandle進行偵聽
當一個可取消的操作在等待一個同步原語(如System.Threading. manualresetevent或System.Threading. Semaphore)時可能會阻塞。
你可以使用CancellationToken.WaitHandle屬性,以使操作同時等待事件和取消請求。
CancellationToken的 等待句柄 將在響應(yīng)取消請求時發(fā)出信號,該方法可以使用WaitAny()方法的返回值來確定發(fā)出信號的是否是cancellation token。然后操作可以直接退出,或者拋出OperationCanceledException異常。
// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
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);下面的示例使用ManualResetEvent來演示如何解除阻塞不支持統(tǒng)一取消的等待句柄。
using System;
using System.Threading;
using System.Threading.Tasks;
class CancelOldStyleEvents
{
// Old-style MRE that doesn't support unified cancellation.
static ManualResetEvent mre = new ManualResetEvent(false);
static void Main()
{
var cts = new CancellationTokenSource();
// Pass the same token source to the delegate and to the task instance.
Task.Run(() => DoWork(cts.Token), cts.Token);
Console.WriteLine("Press s to start/restart, p to pause, or c to cancel.");
Console.WriteLine("Or any other key to exit.");
// Old-style UI thread.
bool goAgain = true;
while (goAgain)
{
char ch = Console.ReadKey(true).KeyChar;
switch (ch)
{
case 'c':
cts.Cancel();
break;
case 'p':
mre.Reset();
break;
case 's':
mre.Set();
break;
default:
goAgain = false;
break;
}
Thread.Sleep(100);
}
cts.Dispose();
}
static void DoWork(CancellationToken token)
{
while (true)
{
// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
new TimeSpan(0, 0, 20));
// Were we canceled while waiting?
if (eventThatSignaledIndex == 1)
{
Console.WriteLine("The wait operation was canceled.");
throw new OperationCanceledException(token);
}
// Were we canceled while running?
else if (token.IsCancellationRequested)
{
Console.WriteLine("I was canceled while running.");
token.ThrowIfCancellationRequested();
}
// Did we time out?
else if (eventThatSignaledIndex == WaitHandle.WaitTimeout)
{
Console.WriteLine("I timed out.");
break;
}
else
{
Console.Write("Working... ");
// Simulating work.
Thread.SpinWait(5000000);
}
}
}
}下面的示例使用ManualResetEventSlim來演示如何解除支持統(tǒng)一取消的協(xié)調(diào)原語的阻塞。同樣的方法也可以用于其他輕量級協(xié)調(diào)原語,如SemaphoreSlim和CountdownEvent。
using System;
using System.Threading;
using System.Threading.Tasks;
class CancelNewStyleEvents
{
// New-style MRESlim that supports unified cancellation
// in its Wait methods.
static ManualResetEventSlim mres = new ManualResetEventSlim(false);
static void Main()
{
var cts = new CancellationTokenSource();
// Pass the same token source to the delegate and to the task instance.
Task.Run(() => DoWork(cts.Token), cts.Token);
Console.WriteLine("Press c to cancel, p to pause, or s to start/restart,");
Console.WriteLine("or any other key to exit.");
// New-style UI thread.
bool goAgain = true;
while (goAgain)
{
char ch = Console.ReadKey(true).KeyChar;
switch (ch)
{
case 'c':
// Token can only be canceled once.
cts.Cancel();
break;
case 'p':
mres.Reset();
break;
case 's':
mres.Set();
break;
default:
goAgain = false;
break;
}
Thread.Sleep(100);
}
cts.Dispose();
}
static void DoWork(CancellationToken token)
{
while (true)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("Canceled while running.");
token.ThrowIfCancellationRequested();
}
// Wait on the event to be signaled
// or the token to be canceled,
// whichever comes first. The token
// will throw an exception if it is canceled
// while the thread is waiting on the event.
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);
}
}
}同時監(jiān)聽多個令牌
在某些情況下,偵聽器必須同時偵聽多個cancellation token。
例如,一個可取消操作除了監(jiān)控通過方法形參傳入的外部token之外,還可能必須監(jiān)視內(nèi)部的cancellation token。為此,創(chuàng)建一個linked token源,它可以將兩個或多個token連接到一個token中,如下面的示例所示。
using System;
using System.Threading;
using System.Threading.Tasks;
class LinkedTokenSourceDemo
{
static void Main()
{
WorkerWithTimer worker = new WorkerWithTimer();
CancellationTokenSource cts = new CancellationTokenSource();
// Task for UI thread, so we can call Task.Wait wait on the main thread.
Task.Run(() =>
{
Console.WriteLine("Press 'c' to cancel within 3 seconds after work begins.");
Console.WriteLine("Or let the task time out by doing nothing.");
if (Console.ReadKey(true).KeyChar == 'c')
cts.Cancel();
});
// Let the user read the UI message.
Thread.Sleep(1000);
// Start the worker task.
Task task = Task.Run(() => worker.DoWork(cts.Token), cts.Token);
try
{
task.Wait(cts.Token);
}
catch (OperationCanceledException e)
{
if (e.CancellationToken == cts.Token)
Console.WriteLine("Canceled from UI thread throwing OCE.");
}
catch (AggregateException ae)
{
Console.WriteLine("AggregateException caught: " + ae.InnerException);
foreach (var inner in ae.InnerExceptions)
{
Console.WriteLine(inner.Message + inner.Source);
}
}
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
cts.Dispose();
}
}
class WorkerWithTimer
{
CancellationTokenSource internalTokenSource = new CancellationTokenSource();
CancellationToken internalToken;
CancellationToken externalToken;
Timer timer;
public WorkerWithTimer()
{
// A toy cancellation trigger that times out after 3 seconds
// if the user does not press 'c'.
timer = new Timer(new TimerCallback(CancelAfterTimeout), null, 3000, 3000);
}
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();
}
}
}
}
private void DoWorkInternal(CancellationToken token)
{
for (int i = 0; i < 1000; i++)
{
if (token.IsCancellationRequested)
{
// We need to dispose the timer if cancellation
// was requested by the external token.
timer.Dispose();
// Throw the exception.
token.ThrowIfCancellationRequested();
}
// Simulating work.
Thread.SpinWait(7500000);
Console.Write("working... ");
}
}
public void CancelAfterTimeout(object? state)
{
Console.WriteLine("\r\nTimer fired.");
internalTokenSource.Cancel();
timer.Dispose();
}
}注意,當您完成對鏈接的令牌源的處理后,必須對它調(diào)用Dispose。
當linked token拋出一個操作消連時,傳遞給異常的token就是linked token,而不是前任token。為了確定token的哪個被取消,請直接檢查前任token的狀態(tài)。
在本例中,AggregateException不應(yīng)該被拋出,但這里會捕獲它,因為在實際場景中,除了從任務(wù)委托拋出的OperationCanceledException之外,任何其他異常都被包裝在AggregateException中。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
c# 給button添加不規(guī)則的圖片以及用pictureBox替代button響應(yīng)點擊事件的方法
這篇文章介紹了c# 給button添加不規(guī)則的圖片以及用pictureBox替代button響應(yīng)點擊事件的方法,有需要的朋友可以參考一下2013-09-09
C#使用Socket實現(xiàn)發(fā)送和接收圖片的方法
這篇文章主要介紹了C#使用Socket實現(xiàn)發(fā)送和接收圖片的方法,涉及C#操作socket發(fā)送與接收文件的使用技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-04-04
C# DoubleClick與MouseDoubleClick區(qū)別,雙擊事件引發(fā)順序
從邏輯上來說,由于比MouseDoubleClick 描述更抽象,DoubleClick 事件是控件的更高級別的事件2009-09-09

