I have a situation where a call to CancellationTokenSource.Cancel
never returns. Instead, after Cancel
is called (and before it returns) the execution continues with the cancellation code of the code that is being cancelled. If the code that is cancelled does not subsequently invoke any awaitable code then the caller that originally called Cancel
never gets control back. This is very strange. I would expect Cancel
to simply record the cancellation request and return immediately independent on the cancellation itself. The fact that the thread where Cancel
is being called ends up executing code that belongs to the operation that is being cancelled and it does so before returning to the caller of Cancel
looks like a bug in the framework.
Here is how this goes:
There is a piece of code, let’s call it “the worker code” that is waiting on some async code. To make things simple let’s say this code is awaiting on a Task.Delay:
try { await Task.Delay(5000, cancellationToken); // … } catch (OperationCanceledException) { // …. }
Just before “the worker code” invokes Task.Delay
it is executing on thread T1.
The continuation (that is the line following the “await” or the block inside the catch) will be executed later on either T1 or maybe on some other thread depending on a series of factors.
- There is another piece of code, let’s call it “the client code” that decides to cancel the
Task.Delay
. This code callscancellationToken.Cancel
. The call toCancel
is made on thread T2.
I would expect thread T2 to continue by returning to the caller of Cancel
. I also expect to see the content of catch (OperationCanceledException)
executed very soon on thread T1 or on some thread other than T2.
What happens next is surprising. I see that on thread T2, after Cancel
is called, the execution continues immediately with the block inside catch (OperationCanceledException)
. And that happens while the Cancel
is still on the callstack. It is as if the call to Cancel
is hijacked by the code that it is being cancelled. Here's a screenshot of Visual Studio showing this call stack:
More context
Here is some more context about what the actual code does:
There is a “worker code” that accumulates requests. Requests are being submitted by some “client code”. Every few seconds “the worker code” processes these requests. The requests that are processed are eliminated from the queue.
Once in a while however, “the client code” decides that it reached a point where it wants requests to be processed immediately. To communicate this to “the worker code” it calls a method Jolt
that “the worker code” provides. The method Jolt
that is being called by “the client code” implements this feature by cancelling a Task.Delay
that is executed by the worker’s code main loop. The worker’s code has its Task.Delay
cancelled and proceeds to process the requests that were already queued.
The actual code was stripped down to its simplest form and the code is available on GitHub.
Environment
The issue can be reproduced in console apps, background agents for Universal Apps for Windows and background agents for Universal Apps for Windows Phone 8.1.
The issue cannot be reproduced in Universal apps for Windows where the code works as I would expect and the call to Cancel
returns immediately.
await Task.Delay(...)
, so the continuation triggered byCancellationTokenSource.Cancel
is asynchronously posted to that context. Hence, there's no deadlock. – Eldwun