[英]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:这是如何进行的:
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 或其他线程上执行,具体取决于一系列因素。
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 的问题。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.