CancellationTokenSource.Cancel()挂起

时间:2018-09-21 20:11:31

标签: c# .net async-await cancellationtokensource

当其中一个异步处于活动循环中时,我发现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#是否可以保证曾经产生的异步将永远不会阻止其他异步?

1 个答案:

答案 0 :(得分:2)

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