簡體   English   中英

對 CancellationTokenSource.Cancel 的調用永遠不會返回

[英]A call to CancellationTokenSource.Cancel never returns

我遇到過調用CancellationTokenSource.Cancel永遠不會返回的情況。 相反,在調用Cancel之后(並且在它返回之前),執行會繼續執行被取消代碼的取消代碼。 如果被取消的代碼隨后沒有調用任何可等待的代碼,那么最初調用Cancel的調用者永遠不會重新獲得控制權。 這很奇怪。 我希望Cancel簡單地記錄取消請求並立即獨立於取消本身返回。 調用Cancel的線程最終會執行屬於被取消操作的代碼,並且在返回給Cancel的調用者之前這樣做,這一事實看起來像是框架中的錯誤。

這是如何進行的:

  1. 有一段代碼,我們稱之為“工作代碼”,它正在等待一些異步代碼。 為簡單起見,假設此代碼正在等待 Task.Delay:

     try { await Task.Delay(5000, cancellationToken); // … } catch (OperationCanceledException) { // …. }

就在“工作代碼”調用Task.Delay它正在線程 T1 上執行。 延續(即“await”之后的行或 catch 中的塊)稍后將在 T1 或其他線程上執行,具體取決於一系列因素。

  1. 還有一段代碼,我們稱之為“客戶端代碼”,它決定取消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方法,並且延遲為零。 這將允許取消在不同的上下文中傳播。

CancelAfter 的源代碼在這里。

它在內部使用 TimerQueueTimer 來發出取消請求。 這沒有記錄,但應該可以解決 op 的問題。

文檔在這里。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM