在将包装器写入任意可取消代码的过程中,该代码应该在某个条件成立时运行(必须定期验证),我在CancellationTokenSource
,{{{{}}之间的交互中遇到了有趣的行为。 1}}和Threading.Timer
/ async
生成的代码。
简而言之,看起来如果您有一些可取消的await
正在等待,然后您从Task
回调中取消Task
,代码取消任务后面的操作作为取消请求本身的一部分执行。
在下面的程序中,如果添加跟踪,您将看到Timer
调用中Timer
回调块的执行,以及等待该任务取消的等待任务之后的代码在与cts.Cancel()
调用本身相同的线程中执行。
以下程序正在执行以下操作:
cts.Cancel()
“工作”,然后休眠500ms以证明计时器处理等待此事; 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)
是避免死锁的最佳选择吗?
答案 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
),这是不推荐的。