考虑以下简化示例(准备好转入linqpad,需要提升帐户):

void Main()
{
    Go();
    Thread.Sleep(100000);
}
async void Go()
{
    TcpListener listener = new TcpListener(IPAddress.Any, 6666);
    try
    {
        cts.Token.Register(() => Console.WriteLine("Token was canceled"));
        listener.Start();
        using(TcpClient client = await listener.AcceptTcpClientAsync()
                                               .ConfigureAwait(false))
        using(var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
        {
            var stream=client.GetStream();
            var buffer=new byte[64];
            try
            {
                var amtRead = await stream.ReadAsync(buffer,
                                                     0,
                                                     buffer.Length,
                                                     cts.Token);
                Console.WriteLine("finished");
            }
            catch(TaskCanceledException)
            {
                Console.WriteLine("boom");
            }
        }
    }
    finally
    {
        listener.Stop();
    }
}

如果我将telnet客户端连接到localhost:6666并且无所事事地坐了5秒钟,为什么我看到“token was cancelled”却从来没有看到“boom”(或“finished”)?
此网络流不考虑取消吗?
我可以用Task.Delay()Task.WhenAny的组合来解决这个问题,但我更希望它按预期工作。
相反,下面的取消示例:
async void Go(CancellationToken ct)
{
    using(var cts=new CancellationTokenSource(TimeSpan.FromSeconds(5)))
    {
        try
        {
            await Task.Delay(TimeSpan.FromSeconds(10),cts.Token)
                                        .ConfigureAwait(false);
        }
        catch(TaskCanceledException)
        {
            Console.WriteLine("boom");
        }
    }
}

按预期打印“boom”。发生什么事?

最佳答案

不,NetworkStream不支持取消。
不幸的是,底层win32api并不总是支持每次操作取消。传统上,您可以取消特定句柄的所有I/O,但the method to cancel a single I/O operation是最近才出现的。大多数.net bcl是针对xp api(或更早版本)编写的,xpapi不包括CancelIoEx
Stream通过“伪造”对取消(以及异步I/O)的支持来解决这个问题,即使实现不支持它。对取消的“假”支持只是立即检查令牌,然后启动无法取消的常规异步读取。这就是你看到的NetworkStream发生的事情。
对于sockets(和大多数win32类型),传统的方法是在您想中止通信时关闭句柄。这会导致所有当前操作(包括读取和写入)失败。从技术上讲,这违反了所记录的BCL线程安全性,但确实有效。

cts.Token.Register(() => client.Close());
...
catch (ObjectDisposedException)

另一方面,如果您想检测一个半开放的场景(您的一方正在读取,但另一方已失去连接),那么最好的解决方案是定期发送数据。我更详细地描述了这一点。

09-20 09:46