特定の条件が成立している間 (定期的に検証する必要があります) 実行することになっている任意のキャンセル可能なコードのラッパーを作成する過程で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)
、デッドロックを回避するための最良のオプションはありますか?