Race condition with CancellationToken where Cancel

2019-02-14 12:31发布

问题:

Consider a Winforms application, where we have a button that generates some results. If the user presses the button a second time, it should cancel the first request to generate results and start a new one.

We're using the below pattern, but we are unsure if some of the code is necessary to prevent a race condition (see the commented out lines).

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

Notice:

  • We only ever cancel the CancellationTokenSource on the main thread.
  • We use the same CancellationToken in the continuation as we do in the original task.

We're wondering whether or not the following sequence of events is possible:

  1. User clicks "generate results" button. Initial task t1 is started.
  2. User clicks "generate results" button again. Windows message is posted to queue, but the handler hasn't been executed yet.
  3. Task t1 finishes.
  4. TPL starts prepares to start the continuation (since the CancellationToken is not cancelled yet). The task scheduler posts the work to the Windows message queue (to get it to run on the main thread).
  5. The generateResultsButton_Click for the 2nd click starts executing and the CancellationTokenSource is cancelled.
  6. The continuations work starts and it operates as though the token were not cancelled (i.e. it displays its results in the UI).

So, I think the question boils down to:

When work is posted to the main thread (by using TaskScheduler.FromCurrentSynchronizationContext()) does the TPL check the CancellationToken on the main thread before executing the task's action, or does it check the cancellation token on whatever thread it happens to be on, and then post the work to the SynchronizationContext?

回答1:

Assuming I read the question correctly, you are worried about the following sequence of events:

  1. The button is clicked, task T0 is scheduled on the thread pool, continuation C0 is scheduled as a continuation of T0, to be run on the synchronization context's task scheduler
  2. The button is clicked again. Let's say the message pump is busy doing something else, so now the message queue consists of one item, the click handler.
  3. T0 completes, this causes C0 to be posted to the message queue. The queue now contains two items, the click handler and the execution of C0.
  4. The click handler message is pumped, and the handler signals the token driving the cancellation of T0 and C0. Then it schedules T1 on the thread pool and C1 as a continuation in the same manner as step 1.
  5. The 'execute C0' message is still in the queue, so it gets processed now. Does it execute the continuation you intended to cancel?

The answer is no. TryExecuteTask will not execute a task which has been signaled for cancellation. It's implied by that documentation, but spelled out explicitly on the TaskStatus page, which specifies

Canceled -- The task acknowledged cancellation by throwing an OperationCanceledException with its own CancellationToken while the token was in signaled state, or the task's CancellationToken was already signaled before the task started executing.

So at the end of the day T0 will be in the RanToCompletion state and C0 will be in the Canceled state.

This is all, of course, assuming that the current SynchronizationContext does not allow tasks to be run concurrently (as you are aware, the Windows Forms one does not -- I'm just noting that this is not a requirement of synchronization contexts)

Also, it's worth noting that the exact answer to your final question about whether the cancellation token is checked in the context of when cancellation is requested or when the task is executed, the answer is really both. In addition to the final check in TryExecuteTask, as soon as cancellation is requested the framework will call TryDequeue, an optional operation that task schedulers can support. The synchronization context scheduler does not support it. But if it somehow did, the difference might be that the 'execute C0' message would be ripped out of the thread's message queue entirely and it wouldn't even try to execute the task.



回答2:

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.