考虑以下简化示例(准备在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")?
此NetworkStream是否不尊重取消?
我可以通过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
不支持取消。
不幸的是,底层Win32 API并不总是支持每次操作取消。传统上,您可以取消特定句柄的所有I/O,但取消单个I/O操作的方法是最近才出现的。大多数.NET BCL都是针对XP API(或更早版本)编写的,其中不包括CancelIoEx
。
Stream
通过"伪造"对取消(以及异步I/O)的支持来加剧这个问题,即使实现不支持它。对取消的"伪造"支持只是立即检查令牌,然后启动无法取消的常规异步读取。这就是您在NetworkStream
中看到的情况。
对于套接字(以及大多数Win32类型),如果您想中止通信,传统的方法是关闭句柄。这会导致所有当前操作(读取和写入)失败。从技术上讲,这违反了BCL线程安全性,但它确实有效。
cts.Token.Register(() => client.Close());
...
catch (ObjectDisposedException)
另一方面,如果您想检测半开放的场景(您的一侧正在读取,但另一侧已失去连接),那么最好的解决方案是定期发送数据。我在博客上详细描述了这一点。