4

考虑一个 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在 continuation 中使用与在原始任务中相同的方法。

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

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

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

当工作发布到主线程时(通过使用TaskScheduler.FromCurrentSynchronizationContext()),TPLCancellationToken在执行任务的操作之前检查主线程上的SynchronizationContext?

4

2 回答 2

5

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

  1. 单击按钮,任务T0被安排在线程池中,continuationC0被安排为 的 continuation T0,在同步上下文的任务调度程序上运行
  2. 再次单击该按钮。假设消息泵正忙于做其他事情,所以现在消息队列由一项组成,即点击处理程序。
  3. T0完成,这会导致C0被发布到消息队列。队列现在包含两个项目,点击处理程序和C0.
  4. 点击处理程序消息被抽出,处理程序向驱动取消T0and的令牌发出信号C0。然后它以与 step 相同的方式T1在线程池上调度并C1作为延续1
  5. 'execute C0' 消息仍在队列中,因此它现在得到处理。它是否执行您打算取消的延续?

答案是不。TryExecuteTask不会执行已发出取消信号的任务。该文档暗示了这一点,但在TaskStatus页面上明确说明,该页面指定

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

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

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

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

于 2013-03-28T02:13:20.200 回答
-1

The way I see it, regardless of which thread checks the CencellationToken, you have to consider the possibility that your continuation can get scheduled and the user can cancel the request while the continuation is being executed. So I think the check that was commented out should be checked and should probably be checked AGAIN after reading the result:

        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());

I would also add a continutation to handle the cancellation condition separately:

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

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

This way you have all possibilities covered.

于 2013-03-27T15:55:35.230 回答