2013-03-27 2 views
3

Рассмотрите приложение Winforms, где у нас есть кнопка, которая генерирует некоторые результаты. Если пользователь нажимает кнопку второй раз, он должен отменить первый запрос для генерации результатов и начать новый.Условие гонки с CancellationToken, где CancellationTokenSource отменяется только в основной теме

Мы используем рисунок ниже, но мы не уверены, нужен ли какой-либо код для предотвращения состояния гонки (см. Прокомментированные строки).

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. Производится генерацияResultsButton_Click для второго щелчка и CancellationTokenSource отменяется.
  6. Работа продолжения начинается, и она работает так, как будто токен не отменяется (т. Е. Отображает его результаты в пользовательском интерфейсе).

Так что, я думаю, что вопрос сводится к тому:

Когда работа размещена в главном потоке (с помощью TaskScheduler.FromCurrentSynchronizationContext()) делает TPL проверить CancellationToken на главном потоке перед выполнением действия выполнения задачи, или проверяет ли токен отмены на любом потоке, на котором это происходит, а затем отправляет работу на SynchronizationContext?

+0

Я не уверен, о каком «состоянии гонки» вы говорите. Но нет, вам не нужна дополнительная проверка на отмену, потому что вы использовали опцию 'TaskContinuationOptions.OnlyOnRanToCompletion'. –

+0

Мы хотим отменить обновление пользовательского интерфейса даже в том случае, когда задача завершена, но пользователь отменил (путем повторного нажатия кнопки).Я обновил код, чтобы включить очистку ярлыка пользовательского интерфейса, чтобы лучше показать проблему. Как только пользователь нажимает кнопку, мы хотим, чтобы метка была пустой, пока не отобразятся результаты для этого щелчка. –

+0

После продолжения продолжения вы находитесь в потоке пользовательского интерфейса (из-за 'TaskScheduler.FromCurrentSynchronizationContext'). Нить пользовательского интерфейса не может делать ничего другого, пока это продолжение не будет выполнено (один поток, по одному в момент времени). Таким образом, в случае 4/5/6 5 будет * не * происходить до тех пор, пока продолжение не будет завершено (т.е. это не 5, это действительно 6) –

ответ

5

Предполагая, что я прочитал вопрос правильно, вы беспокоитесь о следующей последовательности событий:

  1. кнопка нажата, задача T0 запланирована на пуле потоков, продолжение C0 запланировано как продолжение T0 , для запуска в планировщике задач контекста синхронизации
  2. Кнопка снова нажата. Предположим, что насос сообщений занят чем-то другим, поэтому теперь очередь сообщений состоит из одного элемента, обработчика кликов.
  3. T0 завершает, это приводит к тому, что C0 будет отправлен в очередь сообщений. Теперь очередь содержит два элемента: обработчик кликов и выполнение C0.
  4. Передача сообщения обработчика щелчка, и обработчик сигнализирует токен, приводящий к отмене T0 и C0. Затем он планирует T1 в пуле потоков и C1 в качестве продолжения таким же образом, как на этапе 1.
  5. Сообщение «выполнить C0» все еще находится в очереди, поэтому теперь оно обрабатывается. Выполняет ли это продолжение, которое вы хотели отменить?

Ответ отрицательный. TryExecuteTask не выполнит задачу, которая была передана для отмены. Это вытекает из этой документации, но прописано явно на TaskStatus страницы, которая определяет

Отменено - задача признала отмену выбрасывания OperationCanceledException со своим собственным CancellationToken в то время как маркер был в сигнальном состоянии, или функция CancellationToken для задачи была уже указана до начала выполнения задачи.

Так что в конце дня T0 будет в состоянии RanToCompletion и C0 будет находиться в Canceled состоянии.

Это все, конечно, если предположить, что текущий SynchronizationContext не позволяет запускать задачи одновременно (как вам известно, Windows Forms нет - я просто замечаю, что это не требование контексты синхронизации)

Кроме того, стоит отметить, что точный ответ на ваш последний вопрос о том, отмечен маркер отмены в контексте, когда аннулирование испрашивается или когда задание выполнено, ответ действительно как. В дополнение к окончательной проверке в TryExecuteTask, как только будет исправлена ​​аннулирование, фреймворк будет вызывать TryDequeue, необязательную операцию, которую могут поддерживать планировщики задач. Планировщик контекста синхронизации не поддерживает его. Но если это так или иначе, разница может заключаться в том, что сообщение «execute C0» будет вырвано из очереди сообщений потока, и оно даже не попытается выполнить задачу.

+1

Отличный ответ. Итак, ключ заключается в том, что TryExecuteTask будет выполняться в основном потоке (в моем конкретном случае), а в TryExecuteTask будет проверяться токен отмены. Это соответствует тому, что показано здесь: http://blogs.msdn.com/b/pfxteam/archive/2009/09/22/9898090.aspx?Redirected=true –

+0

О, и добро пожаловать в StackOverflow. :) –

-1

Как я вижу это, независимо от того, какой поток проверяет 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()); 

Я хотел бы также добавить continutation для обработки условия отмены отдельно:

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

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

Таким образом, у вас есть все возможности.

+0

Обратите внимание, что (a) мое продолжение выполняется в основном потоке и что (b) мой CancellationTokenSource только отменяется в основном потоке. Таким образом, нет, CancellationToken не может быть отменен, пока действие моего продолжения работает. –

+0

Кроме того, в случае отмены нечего делать, поэтому нет необходимости в продолжении для этого. –

+0

Если код достигает продолжения, предыдущая задача выполнена успешно. Не должно возникать никаких проблем с продолжением, несмотря на отмену, потому что «Результат» будет иметь достоверные данные. –