我有几个异步网络操作返回可能永远不会完成的任务:
UdpClient.ReceiveAsync
不接受CancellationToken
TcpClient.GetStream
返回的NetworkStream
不尊重CancellationToken
上的Stream.ReadAsync
(仅在操作开始时检查取消)两者都在等待可能永远不会来的消息(例如因丢包或没有响应)。这意味着我有幻像任务永远不会完成,永远不会运行的延续和使用套接字。我知道我可以使用TimeoutAfter
,但这只会解决延续问题。
那我该怎么办?
答案 0 :(得分:7)
所以我在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();
}
}
答案 1 :(得分:3)
那我该怎么办?
在这种特殊情况下,我宁愿使用UdpClient.Client.ReceiveTimeout
和TcpClient.ReceiveTimeout
来优雅地超时UDP或TCP接收操作。我希望从套接字发出超时错误,而不是来自任何外部源。
除此之外我还需要观察其他一些取消事件,比如点按用户界面按钮,我只会使用Stephen Toub的"How do I cancel non-cancelable async operations?"中的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);
// ...
}
IMO,这更清楚地表明了我作为开发人员的意图,并且对第三方更具可读性。另外,我不需要捕获ObjectDisposedException
例外。我仍然需要在我的客户端代码中的某个地方观察OperationCanceledException
,但是无论如何我都会这样做。 OperationCanceledException
通常会脱离其他例外,我可以选择OperationCanceledException.CancellationToken
来查看取消原因。
除此之外,与@ I3arnon的回答没有多大区别。我只是觉得我不需要另外一种模式,因为我已经拥有WithCancellation
。
进一步说明意见:
OperationCanceledException
,即:
async void Button_Click(sender o, EventArgs args)
{
try
{
await DoSocketStuffAsync(_userCancellationToken.Token);
}
catch (Exception ex)
{
while (ex is AggregateException)
ex = ex.InnerException;
if (ex is OperationCanceledException)
return; // ignore if cancelled
// report otherwise
MessageBox.Show(ex.Message);
}
}
WithCancellation
电话中使用ReadAsync
,我喜欢这个事实,原因如下。首先,我可以创建扩展程序ReceiveAsyncWithToken
:
public static class UdpClientExt
{
public static Task<UdpReceiveResult> ReceiveAsyncWithToken(
this UdpClient client, CancellationToken token)
{
return client.ReceiveAsync().WithCancellation(token);
}
}
其次,从现在起3年后,我可能正在审查.NET 6.0的此代码。到那时,Microsoft可能会有一个新的API,UdpClient.ReceiveAsyncWithTimeout
。在我的情况下,我只需将ReceiveAsyncWithToken(token)
或ReceiveAsync().WithCancellation(token)
替换为ReceiveAsyncWithTimeout(timeout, userToken)
。处理CreateTimeoutScope
时不会那么明显。