CancellationTokenSource.Cancel() зависает
Я наблюдаю повесить в CancellationTokenSource.Cancel
когда один из асинхронных находится в активном цикле.
Полный код:
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();
}
Печать Spawned
и висит. Callstack выглядит так:
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
приведет к Spawned\nCancelled
на выходе.
Есть идеи почему? Гарантирует ли C#, что однокорпусная асинхронность никогда не будет блокировать другие асинхронные операции?
1 ответ
CancellationTokenSource
не имеет никакого понятия о планировщике задач. Если обратный вызов не был зарегистрирован в пользовательском контексте синхронизации, CancellationTokenSource выполнит его в том же стеке вызовов, что и .Cancel()
, В вашем случае обратный вызов отмены завершает задачу, возвращенную Task.Delay
, то продолжение встраивается, что приводит к бесконечному циклу внутри CancellationTokenSource.Cancel
,
Ваш пример с Task.Yield
работает только из-за состояния гонки. Когда токен отменен, поток не начал выполняться Task.Delay
следовательно, нет продолжения в строке. Если вы измените свой Main
чтобы добавить паузу, вы увидите, что она все равно будет зависать даже при 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();
}
Прямо сейчас единственный надежный способ защитить звонок CancellationTokenSource.Cancel
это завернуть в Task.Run
,