This behavior is because an async
method's continuation is scheduled with TaskContinuationOptions.ExecuteSynchronously
. I ran into a similar issue and blogged about it here. AFAIK, that's the only place this behavior is documented. (As a side note, it's an implementation detail and could change in the future).
There are a few alternative approaches; you'll have to decide which one is best.
First, is there any way the timer could be replaced by CancelAfter
? Depending on the nature of the work after the cts is cancelled, something like this might work:
async Task CleanupAfterCancellationAsync(CancellationToken token)
{
try { await token.AsTask(); }
catch (OperationCanceledException) { }
await Task.Delay(500); // remainder of the timer callback goes here
}
(using AsTask
from my AsyncEx library; it's not hard to build AsTask
yourself if you prefer)
Then you could use it like this:
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;
...
Or...
You could replace the Timer
with an async
method:
static async Task TimerReplacementAsync(CancellationTokenSource cts)
{
await Task.Delay(100);
cts.Cancel();
await Task.Delay(500); // remainder of the timer callback goes here
}
Used as such:
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;
...
Or...
You could just kick off the cancellation in a Task.Run
:
using (var cancelTimer = new Timer(_ => { Task.Run(() => cts.Cancel()); Thread.Sleep(500); }))
I don't like this solution as well as the others because you still end up with synchronous blocking (ManualResetEvent.WaitOne
) inside an async
method which isn't recommended.