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,

Другие вопросы по тегам