2013-05-18 5 views
0

В процессе написания обертки произвольному отменяемому коду, который должен работать, пока выполняется определенное условие (которое должно проверяться регулярно), я наткнулся на интересные поведение во взаимодействии между CancellationTokenSource, Threading.Timer и async/await сгенерированный код.Тупик от комбинации async/await, CancellationTokenSource, Threading.Timer

В двух словах, что это выглядит как то, что если у вас есть Cancellable Task, что вы ожидали, и тогда вы отмените, что Task от Timer обратного вызова, код, который следует за отмененной задачи выполняется как часть отмены запросить себя.

В программе ниже, если вы добавите трассировки вы увидите, что выполнение Timer обратного вызова блоков в cts.Cancel() вызова, а также о том, что код после долгожданного задачи, которая получает отменен этот вызов выполняется в том же потоке, что и cts.Cancel() называть себя.

Программа ниже делает следующее:

  1. Создать аннулирование маркера источника для отмены работы, что мы собираемся имитировать;
  2. создать таймер, который будет использоваться для отмены работы после ее запуска;
  3. программный таймер, чтобы выйти через 100 мс;
  4. начало работы, только холостой ход на 200 мс;
    • таймер обратного вызова, отменяющий Task.Delay «работа», затем спящий на 500 мс, чтобы продемонстрировать, что таймер захочет этого;
  5. убедитесь, что работа отменена, как ожидалось;
  6. таймер очистки, следя за тем, чтобы таймер не вызывался после этой точки, и если он уже был запущен, мы блокируем его, ожидая его завершения (притворяйтесь, что после этого было больше работы, которая не срабатывала бы должным образом, если бы таймер обратный вызов был запущен одновременно).
namespace CancelWorkFromTimer 
{ 
    using System; 
    using System.Diagnostics; 
    using System.Threading; 
    using System.Threading.Tasks; 

    class Program 
    { 
     static void Main(string[] args) 
     { 
      Stopwatch sw = Stopwatch.StartNew(); 
      bool finished = CancelWorkFromTimer().Wait(2000); 
      Console.WriteLine("Finished in time?: {0} after {1}ms; press ENTER to exit", finished, sw.ElapsedMilliseconds); 
      Console.ReadLine(); 
     } 

     private static async Task CancelWorkFromTimer() 
     { 
      using (var cts = new CancellationTokenSource()) 
      using (var cancelTimer = new Timer(_ => { cts.Cancel(); Thread.Sleep(500); })) 
      { 
       // Set cancellation to occur 100ms from now, after work has already started 
       cancelTimer.Change(100, -1); 
       try 
       { 
        // Simulate work, expect to be cancelled 
        await Task.Delay(200, cts.Token); 
        throw new Exception("Work was not cancelled as expected."); 
       } 
       catch (OperationCanceledException exc) 
       { 
        if (exc.CancellationToken != cts.Token) 
        { 
         throw; 
        } 
       } 
       // Dispose cleanly of timer 
       using (var disposed = new ManualResetEvent(false)) 
       { 
        if (cancelTimer.Dispose(disposed)) 
        { 
         disposed.WaitOne(); 
        } 
       } 

       // Pretend that here we need to do more work that can only occur after 
       // we know that the timer callback is not executing and will no longer be 
       // called. 

       // DO MORE WORK HERE 
      } 
     } 
    } 
} 

Самый простой способ сделать эту работу, как я ожидал, что это работает, когда я первый написал это использовать cts.CancelAfter(0) вместо cts.Cancel(). Согласно документации, cts.Cancel() будет запускать любые зарегистрированные обратные вызовы синхронно, и я предполагаю, что в этом случае при взаимодействии с сгенерированным кодом async/ весь код, который был после того, как произошла отмена, работает как часть этого. cts.CancelAfter(0) отделяет выполнение этих обратных вызовов от собственного исполнения.

Кто-нибудь сталкивался с этим раньше? В таком случае cts.CancelAfter(0) лучший вариант, чтобы избежать тупика?

+0

Если вы исключили блок 'using (var disposed = ...)', он работает так, как ожидалось. Как вы уверены, что это (второе) распоряжение таймером необходимо? –

+0

@HenkHolterman: В этом примере это не имело бы значения, если бы после него не было кода, который не мог быть безопасным, пока мы не узнаем, что обратный вызов таймера больше не работает и не будет вызван снова, тогда этот блок является ([MSDN docs] (http://msdn.microsoft.com/en-us/library/b97tkt95.aspx)) – observer

ответ

1

Это поведение объясняется тем, что продолжение метода async запланировано на TaskContinuationOptions.ExecuteSynchronously. Я столкнулся с аналогичной проблемой и blogged about it here. AFAIK, это единственное место, где это поведение документировано. (Как примечание, это детализация реализации и может измениться в будущем).

Существует несколько альтернативных подходов; вам нужно решить, какой из них лучше.

Во-первых, есть ли способ заменить таймер на CancelAfter? В зависимости от характера работы после каратов отменяется, что-то подобное может работать:

async Task CleanupAfterCancellationAsync(CancellationToken token) 
{ 
    try { await token.AsTask(); } 
    catch (OperationCanceledException) { } 
    await Task.Delay(500); // remainder of the timer callback goes here 
} 

(с использованием AsTask from my AsyncEx library, это не трудно построить AsTask себя, если вы предпочитаете)

Тогда вы могли бы использовать его как это:

var cts = new CancellationTokenSource(); 
var cleanupCompleted = CleanupAfterCancellationAsync(cts.Token); 
cts.CancelAfter(100); 
... 
try 
{ 
    await Task.Delay(200, cts.Token); 
    throw new Exception("Work was not cancelled as expected."); 
} 
catch (OperationCanceledException exc) { } 
await cleanupCompleted; 
... 

Или ...

Вы могли бы заменить Timer с async способом:

static async Task TimerReplacementAsync(CancellationTokenSource cts) 
{ 
    await Task.Delay(100); 
    cts.Cancel(); 
    await Task.Delay(500); // remainder of the timer callback goes here 
} 

Используется как например:

var cts = new CancellationTokenSource(); 
var cleanupCompleted = TimerReplacementAsync(cts); 
... 
try 
{ 
    await Task.Delay(200, cts.Token); 
    throw new Exception("Work was not cancelled as expected."); 
} 
catch (OperationCanceledException exc) { } 
await cleanupCompleted; 
... 

Или ...

Вы могли бы просто начать отмену в Task.Run :

using (var cancelTimer = new Timer(_ => { Task.Run(() => cts.Cancel()); Thread.Sleep(500); })) 

Мне не нравится это решение, а также другие, потому что вы все еще в конечном итоге сталкиваетесь с синхронной блокировкой (ManualResetEvent.WaitOne) внутри метода async, который не рекомендуется.

+0

@StephenClearly: Ваши альтернативы будут хорошо работать для моего образца (что, возможно, упростило бит слишком много), но у меня есть еще несколько ограничений для реального кода, где я нашел эту проблему: таймер предназначен не только для отмены, но и для периодического обновления права собственности на ресурс, от выполнения которого зависит (здесь заменяется на ' Task.Delay'). Если он не может возобновить право собственности на ресурс, он должен как можно скорее прервать работу. Когда работа завершается без отмены, я хочу остановить таймер и любые попытки возобновления, а затем отпустить ресурс. – observer

+0

(продолжение) Для простоты реализации я думаю, что я могу уйти от предположения, что выпуск ресурса всегда будет побеждать обновление. Меня застало врасплох увидеть весь код после выполнения 'Task.Delay' как части вызова« Отмена », который отменил его. Я думаю, что я понимаю, почему сейчас, поэтому я думаю, что я буду исследовать использование 'CancelAfter (0)' из обратного вызова вместо 'Cancel' (который я сам знаю _fixes_ вопрос в образце) и, вероятно, ThreadPool.RegisterWaitForSingleObject' ждет Dispose(). – observer

+0

(продолжение) Конечно, я бы принял во внимание ваши замечательные комментарии о 'ThreadPool.RegisterWaitForSingleObject' в [этой теме] (http://social.msdn.microsoft.com/Forums/en-US/async/thread/ f3f062cb-ac66-4abd-86b0-8d82193ec404 /). :) – observer