![](/img/trans.png)
[英]Is it safe to call CancellationTokenSource.Cancel multiple times?
[英]A call to CancellationTokenSource.Cancel never returns
我遇到過調用CancellationTokenSource.Cancel
永遠不會返回的情況。 相反,在調用Cancel
之后(並且在它返回之前),執行會繼續執行被取消代碼的取消代碼。 如果被取消的代碼隨后沒有調用任何可等待的代碼,那么最初調用Cancel
的調用者永遠不會重新獲得控制權。 這很奇怪。 我希望Cancel
簡單地記錄取消請求並立即獨立於取消本身返回。 調用Cancel
的線程最終會執行屬於被取消操作的代碼,並且在返回給Cancel
的調用者之前這樣做,這一事實看起來像是框架中的錯誤。
這是如何進行的:
有一段代碼,我們稱之為“工作代碼”,它正在等待一些異步代碼。 為簡單起見,假設此代碼正在等待 Task.Delay:
try { await Task.Delay(5000, cancellationToken); // … } catch (OperationCanceledException) { // …. }
就在“工作代碼”調用Task.Delay
它正在線程 T1 上執行。 延續(即“await”之后的行或 catch 中的塊)稍后將在 T1 或其他線程上執行,具體取決於一系列因素。
Task.Delay
。 此代碼調用cancellationToken.Cancel
。 對Cancel
的調用是在線程 T2 上進行的。 我希望線程 T2 繼續返回到Cancel
的調用者。 我還希望看到很快在線程 T1 或除 T2 之外的某個線程上執行的catch (OperationCanceledException)
的內容。
接下來發生的事情令人驚訝。 我看到在線程 T2 上,在調用Cancel
之后,執行會立即繼續執行catch (OperationCanceledException)
的塊。 這發生在Cancel
仍在調用堆棧上時。 就好像對Cancel
的調用被Cancel
的代碼劫持了。 這是顯示此調用堆棧的 Visual Studio 屏幕截圖:
更多背景
以下是有關實際代碼功能的更多上下文: 有一個“工作代碼”可以累積請求。 請求是由一些“客戶端代碼”提交的。 每隔幾秒鍾,“工作代碼”就會處理這些請求。 處理的請求從隊列中消除。 然而,偶爾,“客戶端代碼”決定它達到了它希望立即處理請求的地步。 為了將其傳達給“工作代碼”,它調用了“工作代碼”提供的方法Jolt
。 “客戶端代碼”正在調用的方法Jolt
通過取消由工作Task.Delay
的代碼主循環執行的Task.Delay
來實現此功能。 工作Task.Delay
的代碼取消了它的Task.Delay
並繼續處理已經排隊的請求。
實際代碼被簡化為最簡單的形式,代碼可在 GitHub 上找到。
環境
該問題可以在控制台應用程序、Windows 通用應用程序的后台代理和 Windows Phone 8.1 通用應用程序的后台代理中重現。
該問題無法在適用於 Windows 的通用應用程序中重現,其中代碼按我的預期工作並且對Cancel
的調用立即返回。
CancellationTokenSource.Cancel
不只是設置IsCancellationRequested
標志。
CancallationToken
類有一個Register
方法,它允許您注冊將在取消時調用的回調。 這些回調由CancellationTokenSource.Cancel
調用。
讓我們來看看源代碼:
public void Cancel()
{
Cancel(false);
}
public void Cancel(bool throwOnFirstException)
{
ThrowIfDisposed();
NotifyCancellation(throwOnFirstException);
}
這是NotifyCancellation
方法:
private void NotifyCancellation(bool throwOnFirstException)
{
// fast-path test to check if Notify has been called previously
if (IsCancellationRequested)
return;
// If we're the first to signal cancellation, do the main extra work.
if (Interlocked.CompareExchange(ref m_state, NOTIFYING, NOT_CANCELED) == NOT_CANCELED)
{
// Dispose of the timer, if any
Timer timer = m_timer;
if(timer != null) timer.Dispose();
//record the threadID being used for running the callbacks.
ThreadIDExecutingCallbacks = Thread.CurrentThread.ManagedThreadId;
//If the kernel event is null at this point, it will be set during lazy construction.
if (m_kernelEvent != null)
m_kernelEvent.Set(); // update the MRE value.
// - late enlisters to the Canceled event will have their callbacks called immediately in the Register() methods.
// - Callbacks are not called inside a lock.
// - After transition, no more delegates will be added to the
// - list of handlers, and hence it can be consumed and cleared at leisure by ExecuteCallbackHandlers.
ExecuteCallbackHandlers(throwOnFirstException);
Contract.Assert(IsCancellationCompleted, "Expected cancellation to have finished");
}
}
好的,現在問題是ExecuteCallbackHandlers
可以在目標上下文或當前上下文中執行回調。 我將讓您看一下ExecuteCallbackHandlers
方法源代碼,因為它有點太長而無法包含在此處。 但有趣的部分是:
if (m_executingCallback.TargetSyncContext != null)
{
m_executingCallback.TargetSyncContext.Send(CancellationCallbackCoreWork_OnSyncContext, args);
// CancellationCallbackCoreWork_OnSyncContext may have altered ThreadIDExecutingCallbacks, so reset it.
ThreadIDExecutingCallbacks = Thread.CurrentThread.ManagedThreadId;
}
else
{
CancellationCallbackCoreWork(args);
}
我想現在你開始明白我接下來Task.Delay
了……當然是Task.Delay
。 讓我們看看它的源代碼:
// Register our cancellation token, if necessary.
if (cancellationToken.CanBeCanceled)
{
promise.Registration = cancellationToken.InternalRegisterWithoutEC(state => ((DelayPromise)state).Complete(), promise);
}
嗯……什么是InternalRegisterWithoutEC
方法?
internal CancellationTokenRegistration InternalRegisterWithoutEC(Action<object> callback, Object state)
{
return Register(
callback,
state,
false, // useSyncContext=false
false // useExecutionContext=false
);
}
啊。 useSyncContext=false
- 這解釋了您看到的行為,因為ExecuteCallbackHandlers
使用的TargetSyncContext
屬性將為 false。 由於沒有使用同步上下文,取消是在CancellationTokenSource.Cancel
的調用上下文上執行的。
這是CancellationToken
/ Source
的預期行為。
有點類似於TaskCompletionSource
工作方式, CancellationToken
注冊使用調用線程同步執行。 您可以在CancellationTokenSource.ExecuteCallbackHandlers
時調用的CancellationTokenSource.ExecuteCallbackHandlers
中看到。
使用同一個線程比在ThreadPool
上安排所有這些延續要高效得多。 通常這種行為不是問題,但如果您在鎖內調用CancellationTokenSource.Cancel
則可能會出現問題,因為在鎖仍然被占用時線程被“劫持”。 您可以使用Task.Run
解決此類問題。 您甚至可以將其設為擴展方法:
public static void CancelWithBackgroundContinuations(this CancellationTokenSource)
{
Task.Run(() => CancellationTokenSource.Cancel());
cancellationTokenSource.Token.WaitHandle.WaitOne(); // make sure to only continue when the cancellation completed (without waiting for all the callbacks)
}
由於這里已經列出的原因,我相信您希望實際使用CancellationTokenSource.CancelAfter
方法,並且延遲為零。 這將允許取消在不同的上下文中傳播。
它在內部使用 TimerQueueTimer 來發出取消請求。 這沒有記錄,但應該可以解決 op 的問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.