使用CancellationToken的竞争条件,其中CancellationTokenSource仅在主线程上被取消

Mat*_*ith 4 c# task-parallel-library cancellationtokensource

考虑一个Winforms应用程序,我们有一个生成一些结果的按钮.如果用户第二次按下该按钮,则应取消第一个生成结果的请求并开始新的结果.

我们使用以下模式,但我们不确定是否有必要使用某些代码来防止竞争条件(请参阅注释掉的行).

    private CancellationTokenSource m_cts;

    private void generateResultsButton_Click(object sender, EventArgs e)
    {
        // Cancel the current generation of results if necessary
        if (m_cts != null)
            m_cts.Cancel();
        m_cts = new CancellationTokenSource();
        CancellationToken ct = m_cts.Token;

        // **Edit** Clearing out the label
        m_label.Text = String.Empty;
        // **Edit**

        Task<int> task = Task.Run(() =>
        {
            // Code here to generate results.
            return 0;
        }, ct);

        task.ContinueWith(t =>
        {
            // Is this code necessary to prevent a race condition?
            // if (ct.IsCancellationRequested)
            //     return;

            int result = t.Result;
            m_label.Text = result.ToString();
        }, ct, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext());
    }
Run Code Online (Sandbox Code Playgroud)

注意:

  • 我们只取消CancellationTokenSource了主线程.
  • 我们CancellationToken在原始任务中使用相同的连续方法.

我们想知道以下事件序列是否可能:

  1. 用户点击"生成结果"按钮.初始任务t1开始.
  2. 用户再次单击"生成结果"按钮.Windows消息已发布到队列,但处理程序尚未执行.
  3. 任务t1结束.
  4. TPL 开始 准备开始继续(因为CancellationToken尚未取消).任务计划程序将工作发布到Windows消息队列(以使其在主线程上运行).
  5. 第二次单击的generateResultsButton_Click开始执行,并CancellationTokenSource取消.
  6. 延续工作开始,它的操作就好像令牌没有被取消(即它在UI中显示其结果).

所以,我认为这个问题归结为:

当工作被发布到主线程(通过使用TaskScheduler.FromCurrentSynchronizationContext())时,TPL是否CancellationToken在执行任务的操作之前检查主线程,或者在它发生的任何线程上检查取消令牌,然后将工作发布到SynchronizationContext

Jac*_*cob 5

假设我正确地阅读了这个问题,您会担心以下一系列事件:

  1. 单击该按钮,T0在线程池上调度任务,继续C0安排为继续T0,在同步上下文的任务调度程序上运行
  2. 再次单击该按钮.假设消息泵忙于执行其他操作,所以现在消息队列包含一个项目,即单击处理程序.
  3. T0完成后,这会导致C0发布到消息队列.该队列现在包含两个项目,单击处理程序和执行C0.
  4. 点击处理程序消息被泵送,处理程序发出驱动取消T0和的令牌信号C0.然后它T1在线程池上进行调度,并C1以与步骤相同的方式作为延续1.
  5. "执行C0"消息仍在队列中,因此现在可以处理它.它是否执行您要取消的延续?

答案是不.TryExecuteTask不会执行已发出取消信号的任务.它是由该文档暗示的,但在TaskStatus页面上明确说明,该页面指​​定

已取消 - 任务通过在令牌处于信号状态时抛出具有自己的CancellationToken的OperationCanceledException来确认取消,或者在任务开始执行之前已经发出任务的CancellationToken信号.

因此,在一天结束时T0将在该RanToCompletion州,C0并将在该Canceled州.

当然,这就是假设当前SynchronizationContext不允许任务同时运行(正如您所知,Windows窗体不会 - 我只是注意到这不是同步上下文的要求)

此外,值得注意的是,关于是否在请求取消或执行任务的情况下检查取消令牌的最终问题的确切答案,答案实际上都是.除了最终签入之外TryExecuteTask,一旦请求取消,框架将调用TryDequeue一个任务调度程序可以支持的可选操作.同步上下文调度程序不支持它.但是如果它以某种方式做到了,差异可能是"执行C0"消息将完全从线程的消息队列中删除,甚至不会尝试执行任务.