简体   繁体   English

对 CancellationTokenSource.Cancel 的调用永远不会返回

[英]A call to CancellationTokenSource.Cancel never returns

I have a situation where a call to CancellationTokenSource.Cancel never returns.我遇到过调用CancellationTokenSource.Cancel永远不会返回的情况。 Instead, after Cancel is called (and before it returns) the execution continues with the cancellation code of the code that is being cancelled.相反,在调用Cancel之后(并且在它返回之前),执行会继续执行被取消代码的取消代码。 If the code that is cancelled does not subsequently invoke any awaitable code then the caller that originally called Cancel never gets control back.如果被取消的代码随后没有调用任何可等待的代码,那么最初调用Cancel的调用者永远不会重新获得控制权。 This is very strange.这很奇怪。 I would expect Cancel to simply record the cancellation request and return immediately independent on the cancellation itself.我希望Cancel简单地记录取消请求并立即独立于取消本身返回。 The fact that the thread where Cancel is being called ends up executing code that belongs to the operation that is being cancelled and it does so before returning to the caller of Cancel looks like a bug in the framework.调用Cancel的线程最终会执行属于被取消操作的代码,并且在返回给Cancel的调用者之前这样做,这一事实看起来像是框架中的错误。

Here is how this goes:这是如何进行的:

  1. There is a piece of code, let's call it “the worker code” that is waiting on some async code.有一段代码,我们称之为“工作代码”,它正在等待一些异步代码。 To make things simple let's say this code is awaiting on a Task.Delay:为简单起见,假设此代码正在等待 Task.Delay:

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

Just before “the worker code” invokes Task.Delay it is executing on thread T1.就在“工作代码”调用Task.Delay它正在线程 T1 上执行。 The continuation (that is the line following the “await” or the block inside the catch) will be executed later on either T1 or maybe on some other thread depending on a series of factors.延续(即“await”之后的行或 catch 中的块)稍后将在 T1 或其他线程上执行,具体取决于一系列因素。

  1. There is another piece of code, let's call it “the client code” that decides to cancel the Task.Delay .还有一段代码,我们称之为“客户端代码”,它决定取消Task.Delay This code calls cancellationToken.Cancel .此代码调用cancellationToken.Cancel The call to Cancel is made on thread T2.Cancel的调用是在线程 T2 上进行的。

I would expect thread T2 to continue by returning to the caller of Cancel .我希望线程 T2 继续返回到Cancel的调用者。 I also expect to see the content of catch (OperationCanceledException) executed very soon on thread T1 or on some thread other than T2.我还希望看到很快在线程 T1 或除 T2 之外的某个线程上执行的catch (OperationCanceledException)的内容。

What happens next is surprising.接下来发生的事情令人惊讶。 I see that on thread T2, after Cancel is called, the execution continues immediately with the block inside catch (OperationCanceledException) .我看到在线程 T2 上,在调用Cancel之后,执行会立即继续执行catch (OperationCanceledException)的块。 And that happens while the Cancel is still on the callstack.这发生在Cancel仍在调用堆栈上时。 It is as if the call to Cancel is hijacked by the code that it is being cancelled.就好像对Cancel的调用被Cancel的代码劫持了。 Here's a screenshot of Visual Studio showing this call stack:这是显示此调用堆栈的 Visual Studio 屏幕截图:

调用栈

More context更多背景

Here is some more context about what the actual code does: There is a “worker code” that accumulates requests.以下是有关实际代码功能的更多上下文: 有一个“工作代码”可以累积请求。 Requests are being submitted by some “client code”.请求是由一些“客户端代码”提交的。 Every few seconds “the worker code” processes these requests.每隔几秒钟,“工作代码”就会处理这些请求。 The requests that are processed are eliminated from the queue.处理的请求从队列中消除。 Once in a while however, “the client code” decides that it reached a point where it wants requests to be processed immediately.然而,偶尔,“客户端代码”决定它达到了它希望立即处理请求的地步。 To communicate this to “the worker code” it calls a method Jolt that “the worker code” provides.为了将其传达给“工作代码”,它调用了“工作代码”提供的方法Jolt The method Jolt that is being called by “the client code” implements this feature by cancelling a Task.Delay that is executed by the worker's code main loop. “客户端代码”正在调用的方法Jolt通过取消由工作Task.Delay的代码主循环执行的Task.Delay来实现此功能。 The worker's code has its Task.Delay cancelled and proceeds to process the requests that were already queued.工作Task.Delay的代码取消了它的Task.Delay并继续处理已经排队的请求。

The actual code was stripped down to its simplest form and the code is available on GitHub .实际代码被简化为最简单的形式,代码可在 GitHub 上找到

Environment环境

The issue can be reproduced in console apps, background agents for Universal Apps for Windows and background agents for Universal Apps for Windows Phone 8.1.该问题可以在控制台应用程序、Windows 通用应用程序的后台代理和 Windows Phone 8.1 通用应用程序的后台代理中重现。

The issue cannot be reproduced in Universal apps for Windows where the code works as I would expect and the call to Cancel returns immediately.该问题无法在适用于 Windows 的通用应用程序中重现,其中代码按我的预期工作并且对Cancel的调用立即返回。

CancellationTokenSource.Cancel doesn't simply set the IsCancellationRequested flag. CancellationTokenSource.Cancel不只是设置IsCancellationRequested标志。

The CancallationToken class has a Register method , which lets you register callbacks that will be called on cancellation. CancallationToken类有一个Register方法,它允许您注册将在取消时调用的回调。 And these callbacks are called by CancellationTokenSource.Cancel .这些回调由CancellationTokenSource.Cancel调用。

Let's take a look at the source code :让我们来看看源代码

public void Cancel()
{
    Cancel(false);
}

public void Cancel(bool throwOnFirstException)
{
    ThrowIfDisposed();
    NotifyCancellation(throwOnFirstException);            
}

Here's the NotifyCancellation method:这是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");
    }
}

Ok, now the catch is that ExecuteCallbackHandlers can execute the callbacks either on the target context, or in the current context.好的,现在问题是ExecuteCallbackHandlers可以在目标上下文或当前上下文中执行回调。 I'll let you take a look at the ExecuteCallbackHandlers method source code as it's a bit too long to include here.我将让您看一下ExecuteCallbackHandlers方法源代码,因为它有点太长而无法包含在此处。 But the interesting part is:但有趣的部分是:

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);
}

I guess now you're starting to understand where I'm going to look next... Task.Delay of course.我想现在你开始明白我接下来Task.Delay了……当然是Task.Delay Let's look at its source code :让我们看看它的源代码

// Register our cancellation token, if necessary.
if (cancellationToken.CanBeCanceled)
{
    promise.Registration = cancellationToken.InternalRegisterWithoutEC(state => ((DelayPromise)state).Complete(), promise);
}

Hmmm... what's that InternalRegisterWithoutEC method ?嗯……什么是InternalRegisterWithoutEC方法

internal CancellationTokenRegistration InternalRegisterWithoutEC(Action<object> callback, Object state)
{
    return Register(
        callback,
        state,
        false, // useSyncContext=false
        false  // useExecutionContext=false
     );
}

Argh.啊。 useSyncContext=false - this explains the behavior you're seeing as the TargetSyncContext property used in ExecuteCallbackHandlers will be false. useSyncContext=false - 这解释了您看到的行为,因为ExecuteCallbackHandlers使用的TargetSyncContext属性将为 false。 As the synchronization context is not used, the cancellation is executed on CancellationTokenSource.Cancel 's call context.由于没有使用同步上下文,取消是在CancellationTokenSource.Cancel的调用上下文上执行的。

This is the expected behavior of CancellationToken / Source .这是CancellationToken / Source的预期行为。

Somewhat similar to how TaskCompletionSource works, CancellationToken registrations are executed synchronously using the calling thread.有点类似于TaskCompletionSource工作方式, CancellationToken注册使用调用线程同步执行。 You can see that in CancellationTokenSource.ExecuteCallbackHandlers that gets called when you cancel.您可以在CancellationTokenSource.ExecuteCallbackHandlers时调用的CancellationTokenSource.ExecuteCallbackHandlers中看到。

It's much more efficient to use that same thread than to schedule all these continuations on the ThreadPool .使用同一个线程比在ThreadPool上安排所有这些延续要高效得多。 Usually this behavior isn't a problem, but it can be if you call CancellationTokenSource.Cancel inside a lock as the thread is "hijacked" while the lock is still taken.通常这种行为不是问题,但如果您在锁内调用CancellationTokenSource.Cancel则可能会出现问题,因为在锁仍然被占用时线程被“劫持”。 You can solve such issues by using Task.Run .您可以使用Task.Run解决此类问题。 You can even make it an extension method:您甚至可以将其设为扩展方法:

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)
}

Because of the reasons already listed here, I believe you want to actually utilize the CancellationTokenSource.CancelAfter method with a zero millisecond delay.由于这里已经列出的原因,我相信您希望实际使用CancellationTokenSource.CancelAfter方法,并且延迟为零。 This will allow the cancellation to propagate in a different context.这将允许取消在不同的上下文中传播。

The source code for CancelAfter is here. CancelAfter 的源代码在这里。

Internally it uses a TimerQueueTimer to make the cancel request.它在内部使用 TimerQueueTimer 来发出取消请求。 This is not documented but should resolve op's issue.这没有记录,但应该可以解决 op 的问题。

Documentation here. 文档在这里。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM