Lad*_*adi 11 c# asynchronous task async-await cancellationtokensource
我有一种CancellationTokenSource.Cancel永远不会回来的情况.相反,在Cancel调用之后(并且在它返回之前),执行继续执行被取消的代码的取消代码.如果取消的代码随后不会调用任何等待代码,则最初调用的调用者Cancel永远不会获得控制权.这很奇怪.我希望Cancel只记录取消请求并立即独立返回取消本身.Cancel被调用的线程最终会执行属于正在被取消的操作的代码,并且在返回到Cancel看起来像框架中的错误的调用者之前这样做.
以下是这样的:
有一段代码,我们称之为"工作代码",它正在等待一些异步代码.为了简单起见,我们假设这段代码正在等待Task.Delay:
try
{
await Task.Delay(5000, cancellationToken);
// …
}
catch (OperationCanceledException)
{
// ….
}
Run Code Online (Sandbox Code Playgroud)就在"工作者代码"调用之前,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取消,并继续处理已排队的请求.
实际代码被拆分为最简单的形式,代码可以在GitHub上获得.
环境
该问题可以在控制台应用程序,Universal Apps for Windows的后台代理和适用于Windows Phone 8.1的Universal Apps的后台代理中重现.
该问题无法在Windows的通用应用程序中重现,其中代码按照我的预期运行,并且调用Cancel立即返回.
Luc*_*ski 12
CancellationTokenSource.Cancel不只是设置IsCancellationRequested标志.
本CancallationToken类有一个Register方法,它可以让你注册回调,将在消除被调用.这些回调被称为CancellationTokenSource.Cancel.
我们来看看源代码:
public void Cancel()
{
Cancel(false);
}
public void Cancel(bool throwOnFirstException)
{
ThrowIfDisposed();
NotifyCancellation(throwOnFirstException);
}
Run Code Online (Sandbox Code Playgroud)
这是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");
}
}
Run Code Online (Sandbox Code Playgroud)
好的,现在捕获的是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);
}
Run Code Online (Sandbox Code Playgroud)
我想现在你已经开始明白我接下来会去哪看...... Task.Delay当然.我们来看看它的源代码:
// Register our cancellation token, if necessary.
if (cancellationToken.CanBeCanceled)
{
promise.Registration = cancellationToken.InternalRegisterWithoutEC(state => ((DelayPromise)state).Complete(), promise);
}
Run Code Online (Sandbox Code Playgroud)
嗯......这个InternalRegisterWithoutEC方法是什么?
internal CancellationTokenRegistration InternalRegisterWithoutEC(Action<object> callback, Object state)
{
return Register(
callback,
state,
false, // useSyncContext=false
false // useExecutionContext=false
);
}
Run Code Online (Sandbox Code Playgroud)
哎呀.useSyncContext=false- 这解释了您所看到的行为,因为所TargetSyncContext使用的属性ExecuteCallbackHandlers将是错误的.由于未使用同步上下文,因此取消将在CancellationTokenSource.Cancel调用上下文中执行.
这是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)
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
2364 次 |
| 最近记录: |