17

我有一个电话CancellationTokenSource.Cancel永远不会返回的情况。相反, afterCancel被调用(并且在它返回之前),执行将继续执行被取消代码的取消代码。如果被取消的代码随后没有调用任何可等待的代码,那么最初调用的调用者Cancel永远不会重新获得控制权。这很奇怪。我希望Cancel简单地记录取消请求并独立于取消本身立即返回。事实上,Cancel被调用的线程最终会执行属于被取消操作的代码,并且在返回到调用者之前执行此操作,这Cancel看起来像是框架中的一个错误。

这是怎么回事:

  1. 有一段代码,我们称之为“工作代码”,它正在等待一些异步代码。为了简单起见,假设这段代码正在等待 Task.Delay:

    try
    {
        await Task.Delay(5000, cancellationToken);
        // … 
    }
    catch (OperationCanceledException)
    {
        // ….
    }
    

就在“工作代码”调用之前Task.Delay,它正在线程 T1 上执行。延续(即“等待”之后的行或 catch 内的块)稍后将在 T1 或其他线程上执行,具体取决于一系列因素。

  1. 还有一段代码,我们称之为“客户端代码”,它决定取消Task.Delay. 此代码调用cancellationToken.Cancel. 调用Cancel是在线程 T2 上进行的。

我希望线程 T2 通过返回到Cancel. 我还希望catch (OperationCanceledException)很快在线程 T1 或 T2 以外的某个线程上看到执行的内容。

接下来发生的事情令人惊讶。我看到在线程 T2 上,在Cancel被调用之后,执行立即继续执行,内部的块catch (OperationCanceledException)。当Cancel仍在调用堆栈上时,就会发生这种情况。就好像调用Cancel被取消的代码劫持了一样。这是显示此调用堆栈的 Visual Studio 屏幕截图:

调用堆栈

更多上下文

以下是有关实际代码作用的更多上下文: 有一个累积请求的“工作代码”。一些“客户端代码”正在提交请求。每隔几秒钟,“工作代码”就会处理这些请求。已处理的请求将从队列中删除。然而,偶尔,“客户端代码”会决定它达到了它希望立即处理请求的地步。为了将其传达给“worker code”,它调用了Jolt“worker code”提供的方法。Jolt“客户端代码”正在调用的方法通过取消Task.Delay由工作人员的代码主循环执行的 a 来实现此功能。工作人员的代码已Task.Delay取消并继续处理已排队的请求。

实际代码被精简为最简单的形式,代码可在 GitHub 上找到

环境

该问题可以在控制台应用程序、适用于 Windows 的通用应用程序的后台代理和适用于 Windows Phone 8.1 的通用应用程序的后台代理中重现。

该问题无法在适用于 Windows 的通用应用程序中重现,其中代码按预期工作并且调用Cancel立即返回。

4

3 回答 3

17

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当然。让我们看看它的源代码

// 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- 这解释了您所看到的行为,因为TargetSyncContext使用的属性ExecuteCallbackHandlers将是错误的。CancellationTokenSource.Cancel由于未使用同步上下文,因此在的调用上下文中执行取消。

于 2015-07-18T23:07:09.200 回答
11

这是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)
}
于 2015-07-18T22:55:14.513 回答
2

由于此处已经列出的原因,我相信您希望以CancellationTokenSource.CancelAfter零毫秒的延迟实际使用该方法。这将允许取消在不同的上下文中传播。

CancelAfter 的源代码在这里。

在内部,它使用 TimerQueueTimer 发出取消请求。这没有记录,但应该解决 op 的问题。

文档在这里。

于 2020-10-04T17:58:44.573 回答