async / await,CancellationTokenSource,Threading.Timer组合的死锁

时间:2013-05-18 09:03:59

标签: c# .net timer cancellationtokensource

在将包装器写入任意可取消代码的过程中,该代码应该在某个条件成立时运行(必须定期验证),我在CancellationTokenSource,{{{{}}之间的交互中遇到了有趣的行为。 1}}和Threading.Timer / async生成的代码。

简而言之,看起来如果您有一些可取消的await正在等待,然后您从Task回调中取消Task,代码取消任务后面的操作作为取消请求本身的一部分执行。

在下面的程序中,如果添加跟踪,您将看到Timer调用中Timer回调块的执行,以及等待该任务取消的等待任务之后的代码在与cts.Cancel()调用本身相同的线程中执行。

以下程序正在执行以下操作:

  1. 创建取消令牌来源以取消我们要模拟的工作;
  2. 创建计时器,用于在启动后取消工作;
  3. 程序计时器从现在开始100ms;
  4. 开始说工作,空转200ms;
    • 计时器回调启动,取消cts.Cancel()“工作”,然后休眠500ms以证明计时器处理等待此事;
  5. 验证工作是否按预期取消;
  6. 清理计时器,确保在此之后不会调用计时器,并且如果它已经在运行,我们将在此处阻止等待它完成(假设之后有更多的工作,如果计时器不能正常工作回调同时运行。)
  7. Task.Delay

    使用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生成代码的交互,所有代码都在此之后取消发生就是其中的一部分。 await将这些回调的执行与其自己的执行分离。

    有没有人遇到过这个?在这种情况下,cts.CancelAfter(0)是避免死锁的最佳选择吗?

1 个答案:

答案 0 :(得分:1)

此行为是因为async方法的延续计划为TaskContinuationOptions.ExecuteSynchronously。我遇到了类似的问题和blogged about it here。 AFAIK,这是记录此行为的唯一地方。 (作为附注,它是一个实现细节,将来可能会改变)。

有一些替代方法;你必须决定哪一个是最好的。

首先,有没有办法可以用CancelAfter取代计时器?取消cts之后工作的性质,这样的事情可能有效:

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),这是不推荐的。