I'm observing hang in CancellationTokenSource.Cancel
when one of the async is in an active loop.
Full code:
static async Task doStuff(CancellationToken token)
{
try
{
// await Task.Yield();
await Task.Delay(-1, token);
}
catch (TaskCanceledException)
{
}
while (true) ;
}
static void Main(string[] args)
{
var main = Task.Run(() =>
{
using (var csource = new CancellationTokenSource())
{
var task = doStuff(csource.Token);
Console.WriteLine("Spawned");
csource.Cancel();
Console.WriteLine("Cancelled");
}
});
main.GetAwaiter().GetResult();
}
Prints Spawned
and hangs. Callstack looks like:
ConsoleApp9.exe!ConsoleApp9.Program.doStuff(System.Threading.CancellationToken token) Line 23 C#
[Resuming Async Method]
[External Code]
ConsoleApp9.exe!ConsoleApp9.Program.Main.AnonymousMethod__1_0() Line 34 C#
[External Code]
Uncommeting await Task.Yield
would result in Spawned\nCancelled
in output.
Any ideas why? Does C# guarantee that once-yielded async would never block other asyncs?
CancellationTokenSource
does not have any notion of task scheduler. If the callback wasn't registered with a custom synchronization context, CancellationTokenSource will execute it in the same callstack as .Cancel()
. In your case, the cancellation callback completes the task returned by Task.Delay
, then the continuation is inlined, resulting in an infinite loop inside of CancellationTokenSource.Cancel
.
Your example with Task.Yield
only works because of a race condition. When the token is cancelled, the thread hasn't started executing Task.Delay
, therefore there is no continuation to inline. If you change your Main
to add a pause, you'll see that it'll still freeze even with Task.Yield
:
static void Main(string[] args)
{
var main = Task.Run(() =>
{
using (var csource = new CancellationTokenSource())
{
var task = doStuff(csource.Token);
Console.WriteLine("Spawned");
Thread.Sleep(1000); // Give enough time to reach Task.Delay
csource.Cancel();
Console.WriteLine("Cancelled");
}
});
main.GetAwaiter().GetResult();
}
Right now, the only way reliable to protect a call to CancellationTokenSource.Cancel
is to wrap it in Task.Run
.