我的情况是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
的调用者。我还希望看到catch (OperationCanceledException)
的内容很快就会在线程T1或T2以外的某个线程上执行。
接下来发生的事情令人惊讶。我在线程T2上看到,在调用Cancel
之后,立即继续执行catch (OperationCanceledException)
内的块。当Cancel
仍然在callstack上时会发生这种情况。就好像被Cancel
的调用被代码劫持一样被取消。这是Visual Studio的屏幕截图,显示了这个调用堆栈:
更多背景
以下是关于实际代码的更多上下文:
有一个“工人代码”可以累积请求。某些“客户端代码”正在提交请求。每隔几秒“工作代码”就会处理这些请求。处理的请求将从队列中删除。
然而,偶尔,“客户端代码”决定它到达希望立即处理请求的点。要将此信息传达给“工作人员代码”,它会调用“工作人员代码”提供的方法Jolt
。由“客户端代码”调用的方法Jolt
通过取消由工作者代码主循环执行的Task.Delay
来实现此功能。工作人员的代码已取消Task.Delay
,并继续处理已排队的请求。
实际代码被拆分为最简单的形式,代码为available on GitHub。
环境
此问题可以在控制台应用程序,Universal Apps for Windows的后台代理和适用于Windows Phone 8.1的Universal Apps的后台代理中重现。
此问题无法在适用于Windows的通用应用中重现,其中代码可以正常运行,并且Cancel
的调用会立即返回。
答案 0 :(得分:11)
CancellationTokenSource.Cancel
并不是简单地设置IsCancellationRequested
标志。
CancallationToken
类有Register
method,可让您注册将在取消时调用的回调。这些回调由CancellationTokenSource.Cancel
调用。
让我们来看看source code:
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
method source code,因为它包含在这里有点太长了。但有趣的是:
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
当然。让我们看一下source code:
// Register our cancellation token, if necessary.
if (cancellationToken.CanBeCanceled)
{
promise.Registration = cancellationToken.InternalRegisterWithoutEC(state => ((DelayPromise)state).Complete(), promise);
}
嗯... InternalRegisterWithoutEC
method是什么?
internal CancellationTokenRegistration InternalRegisterWithoutEC(Action<object> callback, Object state)
{
return Register(
callback,
state,
false, // useSyncContext=false
false // useExecutionContext=false
);
}
哎呀。 useSyncContext=false
- 这解释了您所看到的行为,因为TargetSyncContext
中使用的ExecuteCallbackHandlers
属性将为false。由于未使用同步上下文,因此取消将在CancellationTokenSource.Cancel
的呼叫上下文中执行。
答案 1 :(得分:8)
这是CancellationToken
/ Source
的预期行为。
与TaskCompletionSource
的工作原理有些类似,CancellationToken
注册是使用调用线程同步执行的。您可以在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)
}
答案 2 :(得分:0)
由于这里已经列出了原因,我相信您想实际使用具有零毫秒延迟的CancellationTokenSource.CancelAfter
方法。这将允许取消在不同的上下文中传播。
The source code for CancelAfter is here.
在内部,它使用TimerQueueTimer发出取消请求。这没有记录,但是应该可以解决op的问题。