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

时间:2013-03-27 14:08:10

标签: 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());
    }

注意:

  • 我们只取消主线程上的CancellationTokenSource
  • 我们在延续中使用与原始任务相同的CancellationToken

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

  1. 用户点击“生成结果”按钮。初始任务t1已启动。
  2. 用户再次点击“生成结果”按钮。 Windows消息已发布到队列,但处理程序尚未执行。
  3. 任务t1结束。
  4. TPL 启动 准备开始延续(因为CancellationToken尚未取消)。任务计划程序将工作发布到Windows消息队列(以使其在主线程上运行)。
  5. 第二次点击的generateResultsButton_Click开始执行,CancellationTokenSource被取消。
  6. 延续工作开始,它就像令牌未被取消一样运行(即它在UI中显示其结果)。
  7. 所以,我认为这个问题归结为:

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

2 个答案:

答案 0 :(得分:5)

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

  1. 单击该按钮,在线程池上调度任务T0,继续C0被安排为T0的延续,以便在同步上下文的任务调度程序上运行
  2. 再次单击该按钮。假设消息泵忙于执行其他操作,所以现在消息队列包含一个项目,即单击处理程序。
  3. T0完成后,会导致C0发布到邮件队列中。该队列现在包含两个项目,即单击处理程序和C0的执行。
  4. 点击处理程序消息,处理程序发出驱动取消T0C0的令牌信号。然后它以与步骤T1相同的方式在线程池和C1上安排1作为延续。
  5. “执行C0”消息仍在队列中,因此立即处理。它是否执行了您打算取消的延续?
  6. 答案是否定的。 TryExecuteTask将不会执行已发出取消信号的任务。该文档暗示了这一点,但明确在TaskStatus页面上明确说明了

      

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

    所以在一天结束时T0将处于RanToCompletion状态,C0将处于Canceled状态。

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

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

答案 1 :(得分:-1)

我看到它的方式,无论哪个线程检查CencellationToken,您都必须考虑继续进行调度的可能性,并且用户可以在执行延续时取消请求。因此,我认为应该检查已注释掉的检查,并且应该在阅读结果后检查AGAIN:

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

        int result = t.Result;

        if (ct.IsCancellationRequested)
            return;

        m_label.Text = result.ToString();
    }, ct, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.FromCurrentSynchronizationContext());

我还会添加一个连续分别处理取消条件:

        task.ContinueWith(t =>
    {
        // Do whatever is appropriate here.

    }, ct, TaskContinuationOptions.OnlyOnCanceled, TaskScheduler.FromCurrentSynchronizationContext());

通过这种方式,您可以涵盖所有可能性。