CancellationTokenSource.Cancel() 挂起

CancellationTokenSource.Cancel() hangs

我正在观察 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 并挂起。调用堆栈看起来像:

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] 

取消提交 await Task.Yield 将导致输出 Spawned\nCancelled

知道为什么吗? C# 是否保证一次产生的异步永远不会阻塞其他异步?

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 中。