ChannelReader.ReadAsync(CancellationToken) 和 ChannelWriter.WriteAsync(CancellationToken) 不 return 或在发出令牌时抛出

ChannelReader.ReadAsync(CancellationToken) and ChannelWriter.WriteAsync(CancellationToken) don't return or throw when token is signaled

编辑:我正在清理描述,因为我已经确定这也会影响 WriteAsync,而不仅仅是 ReadAsync...

如果其中一个调用当前正在阻塞 - ReadAsync 因为通道是空的,或者 WriteAsync 因为通道已满 - 然后发出取消令牌信号不会导致 return 执行给调用者。 IE。它没有 return 值,也不会抛出。它只会永远阻塞。从另一个线程在通道上调用 Complete 将导致阻塞的调用抛出 ChannelClosedException,但我不清楚为什么发出取消标记信号不足。

更令人困惑的是,该代码实际上像 .NET Fiddle 一样按预期工作,但在 Visual Studio 2019 或命令提示符下(均在 Windows 10 x64).

在下面的示例代码中,取消注释 main 中的 Complete 行将允许干净关闭,但如果没有它,对 WriteAsync 的调用永远不会 returns,因此对Task.WaitAll 从未 returns.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;


public class Program
{
    public static async Task Task1(Channel<String> q1, CancellationToken cancellationToken)
    {
        int idx = 0;

        Console.WriteLine($"Task1 starting");

        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(100);

                string s = $"element{idx++}";

                Console.WriteLine($"Calling write on {s}");
                await q1.Writer.WriteAsync(s, cancellationToken);
                Console.WriteLine($"Write returned for {s}");
            }
            catch (OperationCanceledException)
            {
                Console.WriteLine($"Operation was cancelled");
            }
            catch (ChannelClosedException)
            {
                Console.WriteLine($"Channel was closed");
            }
        }

        //q1.Writer.Complete();
        Console.WriteLine($"Task1 stopping");

    }

    public static async Task Main()
    {
        Console.WriteLine($"Main started");

        var tasklist = new List<Task>();

        var q1 = Channel.CreateBounded<String>(
            new BoundedChannelOptions(10)
            {
                AllowSynchronousContinuations = false,
                SingleReader = true,
                SingleWriter = true,
                FullMode = BoundedChannelFullMode.Wait
            });

        var cts = new CancellationTokenSource(5000);

        tasklist.Add(Task.Run(() => Task1(q1, cts.Token), cts.Token));

        while (!cts.Token.IsCancellationRequested)
        {
            try
            {
                await Task.Delay(10000, cts.Token);
            }
            catch (OperationCanceledException) { }
        }

        //q1.Writer.Complete();

        Console.WriteLine($"Waiting for all tasks to terminate");
        Task.WaitAll(tasklist.ToArray(), CancellationToken.None);
        Console.WriteLine($"All tasks terminated");
    }
}

并且输出:

Main started
Task1 starting
Calling write on element0
Write returned for element0
Calling write on element1
Write returned for element1
Calling write on element2
Write returned for element2
Calling write on element3
Write returned for element3
Calling write on element4
Write returned for element4
Calling write on element5
Write returned for element5
Calling write on element6
Write returned for element6
Calling write on element7
Write returned for element7
Calling write on element8
Write returned for element8
Calling write on element9
Write returned for element9
Calling write on element10
Waiting for all tasks to terminate

原来这根本不是频道的问题。它与导致死锁的取消令牌的串行、同步处理有关。完成频道可以避免这个问题,向主服务员添加 Task.Yield 也是如此。在此处查看更多信息:https://github.com/dotnet/runtime/issues/64051