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);
        }
    }