【问题标题】:Race condition with CancellationToken where CancellationTokenSource is only cancelled on the main thread具有 CancellationToken 的竞争条件,其中 CancellationTokenSource 仅在主线程上取消
【发布时间】:2013-03-27 14:08:10
【问题描述】:

考虑一个 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 中显示其结果)。

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

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

【问题讨论】:

  • 我不确定你在说什么“竞争条件”。但是,不,您不需要额外的取消检查,因为您使用了 TaskContinuationOptions.OnlyOnRanToCompletion 选项。
  • 即使在任务完成但用户已取消(通过重新单击按钮)的情况下,我们也希望取消更新 UI。我更新了代码以包括清除 UI 标签以更好地演示问题。一旦用户单击按钮,我们希望标签为空,直到显示该单击的结果。
  • 一旦延续开始,你就在 UI 线程上(由于TaskScheduler.FromCurrentSynchronizationContext)。在继续完成之前,UI 线程不能做任何其他事情(一个线程,一次做一件事)。因此,在 4/5/6 的情况下,在延续完成之前,不会出现 5(即不是 5,而是真正的 6)
  • 对,我明白了,我可能没有用适当的术语解释它,但是当任务 t1 完成时,TPL 具有控制权并“准备”开始继续(这就是我的意思第四步)。这个准备工作大概不是在主线程上完成的。据推测,它在准备中所做的一件事是在开始继续操作之前检查取消令牌。问题是:这些准备工作是否都发生在后台线程上(然后实际工作会发布到主线程),还是准备工作也会发布到主线程?
  • 这种类型的工作不会在 SynchronizationContext 线程(即您的情况下的 UI 线程)上完成。点击处理和继续不能同时发生。您可以在继续之前或之后获得点击。如果它在之前,您将总是 在延续中获得取消 - 无论之前的 where (任务正在运行或为延续做准备)。不清楚您为什么认为这可能是个问题?

标签: c# task-parallel-library cancellationtokensource


【解决方案1】:

假设我正确阅读了问题,您担心以下事件顺序:

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

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

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

因此,最终T0 将处于RanToCompletion 状态,C0 将处于Canceled 状态。

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

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

【讨论】:

  • 很好的答案。因此,关键是 TryExecuteTask 将在主线程上执行(在我的具体情况下),并且在 TryExecuteTask 中将检查取消令牌。这与此处显示的内容相符:blogs.msdn.com/b/pfxteam/archive/2009/09/22/…
【解决方案2】:

在我看来,无论哪个线程检查 CencellationToken,您都必须考虑您的延续可以被安排并且用户可以在延续执行时取消请求的可能性。所以我认为应该检查被注释掉的检查,并且可能应该在阅读结果后再次检查:

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

这样你就有了所有的可能性。

【讨论】:

  • 请注意(a)我的延续在主线程上运行,并且(b)我的 CancellationTokenSource 只在主线程上被取消。所以,不,当我的继续操作正在运行时,CancellationToken 不能被取消。
  • 另外,在取消的情况下也没什么可做的,所以不需要继续。
  • 如果代码到达continuation,则上一个任务成功。尽管已取消,但继续继续应该没有问题,因为 Result 将具有有效数据。
  • 您必须确定“取消”也意味着取消对 UI 的更新——这对我来说似乎毫无意义。
  • 是的,我们希望取消意味着取消更新 UI。否则,如果用户再次按下“生成结果”按钮,他们将看到 UI 更新并显示(陈旧)结果,并认为这是刚刚点击按钮的新结果,但实际上结果是旧结果。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-12
  • 2021-06-04
  • 1970-01-01
  • 2023-01-30
  • 1970-01-01
  • 2012-11-17
相关资源
最近更新 更多