C# 异步网络操作永远不会完成
我有几个异步网络操作,它们返回的任务可能永远不会完成:C# 异步网络操作永远不会完成,c#,networking,async-await,idisposable,cancellation-token,C#,Networking,Async Await,Idisposable,Cancellation Token,我有几个异步网络操作,它们返回的任务可能永远不会完成: UdpClient.ReceiveAsync不接受CancellationToken TcpClient.GetStream返回一个不尊重Stream.ReadAsync上的CancellationToken的NetworkStream(仅在操作开始时检查取消) 两者都等待可能永远不会出现的消息(例如,由于数据包丢失或没有响应)。这意味着我有永远不会完成的幻影任务,永远不会运行的延续任务,并且使用了挂起的套接字。我知道我可以使用,但这只能解
UdpClient.ReceiveAsync
不接受CancellationToken
TcpClient.GetStream
返回一个不尊重Stream.ReadAsync
上的CancellationToken
的NetworkStream
(仅在操作开始时检查取消)那么我该怎么办呢?所以我在
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接收操作。我希望超时错误来自套接字,而不是来自任何外部源
如果除此之外,我还需要观察一些其他取消事件,如用户界面按钮单击,我只需使用Stephen Toub的
with cancellation
,如下所示:
using (var client = new UdpClient())
{
UdpClient.Client.ReceiveTimeout = 2000;
var result = await client.ReceiveAsync().WithCancellation(userToken);
// ...
}
如果ReceiveTimeout
对ReceiveAsync
没有影响,我仍然会使用取消:
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
execption。我仍然需要观察调用此函数的客户端代码中的OperationCanceledException
,但无论如何我都会这样做OperationCanceledException
通常与其他异常不同,我可以选择选中OperationCanceledException.CancellationToken
以查看取消的原因
除此之外,与@I3arnon的答案没有太大区别。我只是觉得我不需要另一种模式,因为我已经有了自己的选择
进一步回应评论:
- 我只在客户端代码中捕获
操作取消异常
,即:
- 是的,我将在每个
ReadAsync
调用中使用with cancellation
,我喜欢这个事实,原因如下。首先,我可以创建一个扩展名ReceiveAsyncWithToken
:
公共静态类UdpClientExt
{
公共静态任务ReceiveAsyncWithToken(
此UdpClient客户端(CancellationToken令牌)
{
返回client.ReceiveAsync(),带取消(令牌);
}
}
其次,从现在起的3年内,我可能会审查.NET6.0的代码。到那时,微软可能会有一个新的API,UdpClient.ReceiveAsyncWithTimeout
。在我的例子中,我只需将ReceiveAsyncWithToken(token)
或ReceiveAsync().WithCancellation(token)
替换为ReceiveAsyncWithTimeout(timeout,userToken)
。处理CreateTimeoutScope
不是很明显,至少TcpClient最终会抛出一个异常(或在stream.Read中返回0)。这样就不会有任何虚幻的任务了。假设您在应用程序中只创建一个UdpClient
,那么一个虚拟任务不是问题。如果你认为我错了,请发布一个真实的案例(+code),这样我们可以做出具体的回答。@L.B它不会出现在stream.ReadAsync
(添加到问题中)。很抱歉,我已经用TcpClient或UdpClient编写了数以百万计的代码,但我从来都不需要这样的方法(当然是在全天候运行的服务中)。顺便说一句:数据包丢失
不适用于TCPI。请向您保证,我并没有发明我的情况。性能测试时udp端口用完。@L.B,数据包丢失确实适用于TCP,但它对应用层的处理是透明的。超时仅与同步操作有关,而与异步操作无关:“此选项仅适用于同步接收呼叫。如果超过超时时间,Receive方法将抛出SocketException。“从@l3arnon开始,我更新了答案以回应您的评论。使用Cancellation
虽然不会真正取消ReceiveAsync
幻影任务,但它只允许“放弃”它,就像TimeoutAfter
(也是由toub提供的)@l3arnon,它将以与您的DisposableScope
客户端相同的方式取消它。Dispose
将在使用作用域的相应结束时被调用。它将在with Cancellation
抛出TaskCancelledException
时立即结束。这样,我就不必观察ObjectDisposedException
和你一样。我嘲笑了“.NET 6.0”部分。说得好。人们应该希望,到那时,微软不会恢复以前的未被观察到的askeexception行为,因为那时很多人使用带有取消功能的
(不处理被放弃任务中抛出的异常,即在本例中为ObjectDisposedException
)将是自讨苦吃。优雅的hac…嗯…解决方案!我从来没有想到过处理令人不快的坏角色。破坏性的,但非常有效!我想尝试/捕捉_closeScopeAction是毫无意义的(我的第一个倾向是提供一个基于动作的API)。我想任何有问题的东西都应该尽快以最明显的方式暴露出来。@Clay我同意。如果它实际上不能处理异常,它就不应该捕获它。你可能还想看看这个相关的
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);
}
}
public static class UdpClientExt
{
public static Task<UdpReceiveResult> ReceiveAsyncWithToken(
this UdpClient client, CancellationToken token)
{
return client.ReceiveAsync().WithCancellation(token);
}
}