在为任意可取消代码编写包装器的过程中,该代码应该在某个条件成立时运行(必须定期验证),我在 、 和 / 生成的代码之间的交互中遇到了CancellationTokenSource
有趣Threading.Timer
的async
行为await
。
简而言之,它看起来是,如果您有一些可取消Task
的等待,然后您Task
从Timer
回调中取消它,则取消任务之后的代码将作为取消请求本身的一部分执行。
在下面的程序中,如果添加跟踪,您将看到Timer
调用中回调块的执行cts.Cancel()
,并且被该调用取消的等待任务之后的代码与cts.Cancel()
调用本身在同一个线程中执行。
下面的程序正在执行以下操作:
- 为取消我们要模拟的工作创建取消令牌源;
- 创建计时器,用于在工作开始后取消工作;
- 程序计时器从现在开始 100 毫秒;
- 开始说工作,只是空转200ms;
- 计时器回调启动,取消
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
/await
生成代码的交互,在取消发生点之后的所有代码都作为其中的一部分运行。cts.CancelAfter(0)
将这些回调的执行与其自身的执行分离。
有没有人遇到过这个?在这种情况下,cts.CancelAfter(0)
避免死锁的最佳选择是什么?