В процессе написания обертки произвольному отменяемому коду, который должен работать, пока выполняется определенное условие (которое должно проверяться регулярно), я наткнулся на интересные поведение во взаимодействии между CancellationTokenSource
, Threading.Timer
и async
/await
сгенерированный код.Тупик от комбинации async/await, CancellationTokenSource, Threading.Timer
В двух словах, что это выглядит как то, что если у вас есть Cancellable Task
, что вы ожидали, и тогда вы отмените, что Task
от Timer
обратного вызова, код, который следует за отмененной задачи выполняется как часть отмены запросить себя.
В программе ниже, если вы добавите трассировки вы увидите, что выполнение Timer
обратного вызова блоков в cts.Cancel()
вызова, а также о том, что код после долгожданного задачи, которая получает отменен этот вызов выполняется в том же потоке, что и cts.Cancel()
называть себя.
Программа ниже делает следующее:
- Создать аннулирование маркера источника для отмены работы, что мы собираемся имитировать;
- создать таймер, который будет использоваться для отмены работы после ее запуска;
- программный таймер, чтобы выйти через 100 мс;
- начало работы, только холостой ход на 200 мс;
- таймер обратного вызова, отменяющий
Task.Delay
«работа», затем спящий на 500 мс, чтобы продемонстрировать, что таймер захочет этого;
- таймер обратного вызова, отменяющий
- убедитесь, что работа отменена, как ожидалось;
- таймер очистки, следя за тем, чтобы таймер не вызывался после этой точки, и если он уже был запущен, мы блокируем его, ожидая его завершения (притворяйтесь, что после этого было больше работы, которая не срабатывала бы должным образом, если бы таймер обратный вызов был запущен одновременно).
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)
лучший вариант, чтобы избежать тупика?
Если вы исключили блок 'using (var disposed = ...)', он работает так, как ожидалось. Как вы уверены, что это (второе) распоряжение таймером необходимо? –
@HenkHolterman: В этом примере это не имело бы значения, если бы после него не было кода, который не мог быть безопасным, пока мы не узнаем, что обратный вызов таймера больше не работает и не будет вызван снова, тогда этот блок является ([MSDN docs] (http://msdn.microsoft.com/en-us/library/b97tkt95.aspx)) – observer