繁体   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