异步网络操作永远不会结束

8
我有几个异步网络操作,返回一个任务,可能永远不会完成:
  1. UdpClient.ReceiveAsync 不接受 CancellationToken
  2. TcpClient.GetStream 返回一个 NetworkStream,在 Stream.ReadAsync 上不遵守 CancellationToken(仅在操作开始时检查取消)
两者都等待可能永远不会到来的消息(例如由于数据包丢失或无响应)。这意味着我有一些永远不会完成的“幽灵”任务,永远不会运行的后续操作以及保持挂起的已使用套接字。我知道我可以使用 TimeoutAfter,但那只会解决后续问题。
那么我该怎么办?

@L.B 在stream.ReadAsync中不起作用(已添加到问题中)。我同时每秒使用数百次的TCP和UDP。 - i3arnon
抱歉,我使用TcpClient或UdpClient编写了数百万行代码,但从未需要像这样的方法(当然在24/7运行的服务中)。顺便说一下:数据包丢失不适用于TCP。 - L.B
我向您保证,我并没有编造我的情况。在性能测试时,我的UDP端口用尽了。 - i3arnon
我猜这是因为你不了解协议的内部机制,也没有正确使用相关类。我认为如果没有实际问题可谈论,那么就没有必要继续这个讨论了。不管怎样,如果它能解决你的问题,那就足够好了。 - L.B
2
@L.B,数据包丢失确实会影响TCP协议,但这些问题都由传输层处理,对应用层来说是透明的。 - Nathan Ernst
显示剩余3条评论
2个回答

10

因此我已经在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();
    }
}

优雅的黑客解决方案!我从未想过要处理那个有问题的坏演员。虽然很有破坏性,但非常有效!我想尝试/捕获_closeScopeAction(提供基于操作的API时我的第一个倾向)可能是无意义的。我想任何在那里出现错误的东西都应该尽快以最明显的方式暴露出来。 - Clay
@Clay 我同意。如果它实际上无法处理异常,那么就不应该捕获它。您还可以查看这个相关的问题:如果实例已被处理,调用BeginXXX而不调用EndXXX是否安全 - i3arnon
@Blue 可能会。Dispose 应该能够被调用两次。 - i3arnon

3

那么我应该做什么?

在这种情况下,我更喜欢使用UdpClient.Client.ReceiveTimeoutTcpClient.ReceiveTimeout来优雅地超时UDP或TCP接收操作。我希望超时错误来自套接字,而不是任何外部来源。

如果除此之外,我需要观察其他取消事件,比如UI按钮单击,我会像这样使用Stephen Toub的“如何取消不可取消的异步操作?”中的WithCancellation

using (var client = new UdpClient())
{
    UdpClient.Client.ReceiveTimeout = 2000;

    var result = await client.ReceiveAsync().WithCancellation(userToken);
    // ...
}

针对评论提出的问题,如果ReceiveTimeoutReceiveAsync没有影响,我仍然会使用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处理起来就不那么显然了。


@l3arnon,我更新了答案以回应您的评论。 - noseratio - open to work
1
WithCancellation虽然不能真正取消ReceiveAsync幻影任务,但它允许"放弃"它,就像toubTimeoutAfter一样。 - i3arnon
1
@l3arnon,它将以与您的DisposableScope相同的方式取消:只要相应的using作用域结束,就会调用client.Dispose。这样,我就不必像您一样观察ObjectDisposedException了。同时,当WithCancellation抛出TaskCancelledException时,该作用域也会随之结束。 - noseratio - open to work
你需要观察 TaskCancelledException,然后... 我们的答案之间真正的区别是什么呢? - i3arnon
3
我对“.NET 6.0”这部分笑了。说得好。我们希望到那时微软不会恢复旧的未观察到的任务异常行为,因为那样一来,许多使用WithCancellation 的人(不处理被放弃任务中抛出的异常,比如这种情况下的ObjectDisposedException)将会后悔不已。 - Kirill Shlenskiy
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接