0

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

简而言之,它看起来是,如果您有一些可取消Task的等待,然后您TaskTimer回调中取消它,则取消任务之后的代码将作为取消请求本身的一部分执行。

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

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

  1. 为取消我们要模拟的工作创建取消令牌源;
  2. 创建计时器,用于在工作开始后取消工作;
  3. 程序计时器从现在开始 100 毫秒;
  4. 开始说工作,只是空转200ms;
    • 计时器回调启动,取消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/await生成代码的交互,在取消发生点之后的所有代码都作为其中的一部分运行。cts.CancelAfter(0)将这些回调的执行与其自身的执行分离。

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

4

1 回答 1

1

这种行为是因为async方法的延续是用TaskContinuationOptions.ExecuteSynchronously. 我遇到了一个类似的问题,并在这里写了一篇博客。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我的 AsyncEx 库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

于 2013-05-18T18:28:09.283 回答