UdpClient.ReceiveAsync
不接受CancellationToken
TcpClient.GetStream
返回一个NetworkStream
,在Stream.ReadAsync
上不遵守CancellationToken
(仅在操作开始时检查取消)
TimeoutAfter
,但那只会解决后续问题。那么我该怎么办?
UdpClient.ReceiveAsync
不接受 CancellationToken
TcpClient.GetStream
返回一个 NetworkStream
,在 Stream.ReadAsync
上不遵守 CancellationToken
(仅在操作开始时检查取消)TimeoutAfter
,但那只会解决后续问题。因此我已经在IDisposable
上制作了一个扩展方法,它创建了一个CancellationToken
来处理超时并释放连接,这样任务就能完成,一切都可以继续:
public static IDisposable CreateTimeoutScope(this IDisposable disposable, TimeSpan timeSpan)
{
var cancellationTokenSource = new CancellationTokenSource(timeSpan);
var cancellationTokenRegistration = cancellationTokenSource.Token.Register(disposable.Dispose);
return new DisposableScope(
() =>
{
cancellationTokenRegistration.Dispose();
cancellationTokenSource.Dispose();
disposable.Dispose();
});
}
使用非常简单:
try
{
var client = new UdpClient();
using (client.CreateTimeoutScope(TimeSpan.FromSeconds(2)))
{
var result = await client.ReceiveAsync();
// Handle result
}
}
catch (ObjectDisposedException)
{
return null;
}
额外信息:
public sealed class DisposableScope : IDisposable
{
private readonly Action _closeScopeAction;
public DisposableScope(Action closeScopeAction)
{
_closeScopeAction = closeScopeAction;
}
public void Dispose()
{
_closeScopeAction();
}
}
那么我应该做什么?
在这种情况下,我更喜欢使用UdpClient.Client.ReceiveTimeout
和TcpClient.ReceiveTimeout
来优雅地超时UDP或TCP接收操作。我希望超时错误来自套接字,而不是任何外部来源。
如果除此之外,我需要观察其他取消事件,比如UI按钮单击,我会像这样使用Stephen Toub的“如何取消不可取消的异步操作?”中的WithCancellation
:
using (var client = new UdpClient())
{
UdpClient.Client.ReceiveTimeout = 2000;
var result = await client.ReceiveAsync().WithCancellation(userToken);
// ...
}
针对评论提出的问题,如果ReceiveTimeout
对ReceiveAsync
没有影响,我仍然会使用WithCancellation
:
using (var client = new UdpClient())
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(userToken))
{
UdpClient.Client.ReceiveTimeout = 2000;
cts.CancelAfter(2000);
var result = await client.ReceiveAsync().WithCancellation(cts.Token);
// ...
}
在我看来,这种方式更清晰地展示了我作为开发人员的意图,并且对于第三方来说更易读。此外,我不需要捕获 ObjectDisposedException
异常。虽然在调用此函数的客户端代码中仍需观察 OperationCanceledException
,但无论如何我都需要这么做。通常情况下,OperationCanceledException
与其他异常有所区别,而且我还可以检查 OperationCanceledException.CancellationToken
来观察取消原因。
除此之外,与 @I3arnon 的答案相比,没有太大的区别。我觉得不需要为此再使用另一种模式,因为我已经有了可供使用的 WithCancellation
方法。
进一步解释评论:
OperationCanceledException
,例如:ReadAsync
调用中使用 WithCancellation
,并且我喜欢这样做的原因如下。首先,我可以创建一个扩展方法 ReceiveAsyncWithToken
:public static class UdpClientExt
{
public static Task<UdpReceiveResult> ReceiveAsyncWithToken(
this UdpClient client, CancellationToken token)
{
return client.ReceiveAsync().WithCancellation(token);
}
}
其次,三年后我可能会审查.NET 6.0的代码。届时,微软可能会有一个新的API UdpClient.ReceiveAsyncWithTimeout
。在我的情况下,我将简单地用ReceiveAsyncWithTimeout(timeout, userToken)
替换ReceiveAsyncWithToken(token)
或ReceiveAsync().WithCancellation(token)
。但对于CreateTimeoutScope
处理起来就不那么显然了。
WithCancellation
虽然不能真正取消ReceiveAsync
的幻影任务,但它允许"放弃"它,就像toub
的TimeoutAfter
一样。 - i3arnonDisposableScope
相同的方式取消:只要相应的using
作用域结束,就会调用client.Dispose
。这样,我就不必像您一样观察ObjectDisposedException
了。同时,当WithCancellation
抛出TaskCancelledException
时,该作用域也会随之结束。 - noseratio - open to workTaskCancelledException
,然后... 我们的答案之间真正的区别是什么呢? - i3arnonWithCancellation
的人(不处理被放弃任务中抛出的异常,比如这种情况下的ObjectDisposedException)将会后悔不已。 - Kirill Shlenskiy
stream.ReadAsync
中不起作用(已添加到问题中)。我同时每秒使用数百次的TCP和UDP。 - i3arnon数据包丢失
不适用于TCP。 - L.B