[英]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.