调用 CancellationTokenSource.Cancel 从不 returns
A call to CancellationTokenSource.Cancel never returns
我有一种情况,对 CancellationTokenSource.Cancel
的调用从不 return。相反,在调用 Cancel
之后(以及在 returns 之前),执行将继续执行正在取消的代码的取消代码。如果被取消的代码随后没有调用任何可等待的代码,那么最初调用 Cancel
的调用者永远不会取回控制权。这很奇怪。我希望 Cancel
简单地记录取消请求和 return 立即独立于取消本身。事实上,调用 Cancel
的线程最终执行属于被取消操作的代码,并且在 returning 到 Cancel
的调用者之前执行代码看起来像框架中的错误。
事情是这样的:
有一段代码,我们称它为“工作代码”,它正在等待一些异步代码。为了简单起见,假设此代码正在等待 Task.Delay:
try
{
await Task.Delay(5000, cancellationToken);
// …
}
catch (OperationCanceledException)
{
// ….
}
就在“工作代码”调用 Task.Delay
之前,它正在线程 T1 上执行。
continuation(即“await”之后的行或 catch 内的块)稍后将在 T1 或其他线程上执行,具体取决于一系列因素。
- 还有一段代码,我们称之为决定取消
Task.Delay
的“客户端代码”。此代码调用 cancellationToken.Cancel
。对 Cancel
的调用是在线程 T2 上进行的。
我希望线程 T2 继续 returning 到 Cancel
的调用者。我还希望看到 catch (OperationCanceledException)
的内容很快在线程 T1 或 T2 以外的某个线程上执行。
接下来发生的事情令人惊讶。我看到在线程 T2 上,在调用 Cancel
后,执行会立即继续 catch (OperationCanceledException)
中的块。当 Cancel
仍在调用堆栈中时,就会发生这种情况。就好像对 Cancel
的调用被它被取消的代码劫持了一样。这是显示此调用堆栈的 Visual Studio 的屏幕截图:
更多上下文
这里是关于实际代码作用的更多上下文:
有一个累积请求的“工人代码”。一些“客户端代码”正在提交请求。每隔几秒钟,“工作代码”就会处理这些请求。已处理的请求将从队列中删除。
然而,偶尔,“客户端代码”决定它到达了它希望立即处理请求的地步。为了将此信息传达给“工作者代码”,它调用了“工作者代码”提供的方法 Jolt
。由“客户端代码”调用的方法 Jolt
通过取消由 worker 代码主循环执行的 Task.Delay
来实现此功能。工作人员的代码已取消 Task.Delay
并继续处理已经排队的请求。
实际代码被简化为最简单的形式,代码为 available on GitHub。
环境
此问题可以在 Windows 的控制台应用程序、通用应用程序的后台代理和 Windows Phone 8.1 的通用应用程序的后台代理中重现。
此问题无法在 Windows 的通用应用程序中重现,其中代码按我预期的方式工作并且立即调用 Cancel
returns。
这是 CancellationToken
/Source
的预期行为。
有点类似于TaskCompletionSource
的工作方式,CancellationToken
注册是使用调用线程同步执行的。您可以在 CancellationTokenSource.ExecuteCallbackHandlers
中看到,当您取消时它会被调用。
使用同一个线程比在 ThreadPool
上安排所有这些延续要高效得多。通常此行为不是问题,但如果您在锁内调用 CancellationTokenSource.Cancel
时可能会出现问题,因为线程为 "hijacked" 而锁仍处于锁定状态。您可以使用 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.Cancel
不只是设置 IsCancellationRequested
标志。
CancallationToken
class 有一个 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
- 这解释了您看到的行为,因为 ExecuteCallbackHandlers
中使用的 TargetSyncContext
属性 将是错误的。由于不使用同步上下文,取消在CancellationTokenSource.Cancel
的调用上下文中执行。
由于此处已列出的原因,我相信您想实际使用 CancellationTokenSource.CancelAfter
方法,延迟为零毫秒。这将允许取消在不同的上下文中传播。
The source code for CancelAfter is here.
它在内部使用 TimerQueueTimer 发出取消请求。这没有记录在案,但应该可以解决 op 的问题。
我有一种情况,对 CancellationTokenSource.Cancel
的调用从不 return。相反,在调用 Cancel
之后(以及在 returns 之前),执行将继续执行正在取消的代码的取消代码。如果被取消的代码随后没有调用任何可等待的代码,那么最初调用 Cancel
的调用者永远不会取回控制权。这很奇怪。我希望 Cancel
简单地记录取消请求和 return 立即独立于取消本身。事实上,调用 Cancel
的线程最终执行属于被取消操作的代码,并且在 returning 到 Cancel
的调用者之前执行代码看起来像框架中的错误。
事情是这样的:
有一段代码,我们称它为“工作代码”,它正在等待一些异步代码。为了简单起见,假设此代码正在等待 Task.Delay:
try { await Task.Delay(5000, cancellationToken); // … } catch (OperationCanceledException) { // …. }
就在“工作代码”调用 Task.Delay
之前,它正在线程 T1 上执行。
continuation(即“await”之后的行或 catch 内的块)稍后将在 T1 或其他线程上执行,具体取决于一系列因素。
- 还有一段代码,我们称之为决定取消
Task.Delay
的“客户端代码”。此代码调用cancellationToken.Cancel
。对Cancel
的调用是在线程 T2 上进行的。
我希望线程 T2 继续 returning 到 Cancel
的调用者。我还希望看到 catch (OperationCanceledException)
的内容很快在线程 T1 或 T2 以外的某个线程上执行。
接下来发生的事情令人惊讶。我看到在线程 T2 上,在调用 Cancel
后,执行会立即继续 catch (OperationCanceledException)
中的块。当 Cancel
仍在调用堆栈中时,就会发生这种情况。就好像对 Cancel
的调用被它被取消的代码劫持了一样。这是显示此调用堆栈的 Visual Studio 的屏幕截图:
更多上下文
这里是关于实际代码作用的更多上下文:
有一个累积请求的“工人代码”。一些“客户端代码”正在提交请求。每隔几秒钟,“工作代码”就会处理这些请求。已处理的请求将从队列中删除。
然而,偶尔,“客户端代码”决定它到达了它希望立即处理请求的地步。为了将此信息传达给“工作者代码”,它调用了“工作者代码”提供的方法 Jolt
。由“客户端代码”调用的方法 Jolt
通过取消由 worker 代码主循环执行的 Task.Delay
来实现此功能。工作人员的代码已取消 Task.Delay
并继续处理已经排队的请求。
实际代码被简化为最简单的形式,代码为 available on GitHub。
环境
此问题可以在 Windows 的控制台应用程序、通用应用程序的后台代理和 Windows Phone 8.1 的通用应用程序的后台代理中重现。
此问题无法在 Windows 的通用应用程序中重现,其中代码按我预期的方式工作并且立即调用 Cancel
returns。
这是 CancellationToken
/Source
的预期行为。
有点类似于TaskCompletionSource
的工作方式,CancellationToken
注册是使用调用线程同步执行的。您可以在 CancellationTokenSource.ExecuteCallbackHandlers
中看到,当您取消时它会被调用。
使用同一个线程比在 ThreadPool
上安排所有这些延续要高效得多。通常此行为不是问题,但如果您在锁内调用 CancellationTokenSource.Cancel
时可能会出现问题,因为线程为 "hijacked" 而锁仍处于锁定状态。您可以使用 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.Cancel
不只是设置 IsCancellationRequested
标志。
CancallationToken
class 有一个 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
- 这解释了您看到的行为,因为 ExecuteCallbackHandlers
中使用的 TargetSyncContext
属性 将是错误的。由于不使用同步上下文,取消在CancellationTokenSource.Cancel
的调用上下文中执行。
由于此处已列出的原因,我相信您想实际使用 CancellationTokenSource.CancelAfter
方法,延迟为零毫秒。这将允许取消在不同的上下文中传播。
The source code for CancelAfter is here.
它在内部使用 TimerQueueTimer 发出取消请求。这没有记录在案,但应该可以解决 op 的问题。