【问题标题】:UdpClient.ReceiveAsync correct early terminationUdpClient.ReceiveAsync 正确提前终止
【发布时间】:2016-12-07 14:28:03
【问题描述】:

美好的一天。我与 UdpClient 合作并在其上进行了包装。

为了阅读,我有异步方法:

private async Task<byte[]> Receive(UdpClient client, CancellationToken breakToken)
{
    // Выход из async, если произошёл CancellationRequest
    breakToken.ThrowIfCancellationRequested();

    UdpReceiveResult result;
    try
    {
        result = await client.ReceiveAsync().WithCancellation(breakToken);
    }
    catch(OperationCanceledException)
    {
        // Штатная ситуация ручной остановки Task-а
    }

    return result.Buffer;
}

WithCancellation 是我提前终止的扩展方法:

public static async Task<T> WithCancellation<T>(
    this Task<T> task, CancellationToken cancellationToken)
{
    var tcs = new TaskCompletionSource<bool>();

    using (cancellationToken.Register(
        s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
        tcs))
        if (task != await Task.WhenAny(task, tcs.Task))
            throw new OperationCanceledException(cancellationToken);

    return await task;
}

手动读取停止后,当我调用Dispose 时,会出现System.ObjectDisposedExceptionCallStack:

>   System.dll!System.Net.Sockets.UdpClient.EndReceive(System.IAsyncResult asyncResult, ref System.Net.IPEndPoint remoteEP) Unknown
System.dll!System.Net.Sockets.UdpClient.ReceiveAsync.AnonymousMethod__64_1(System.IAsyncResult ar)  Unknown
mscorlib.dll!System.Threading.Tasks.TaskFactory<System.Net.Sockets.UdpReceiveResult>.FromAsyncCoreLogic(System.IAsyncResult iar, System.Func<System.IAsyncResult, System.Net.Sockets.UdpReceiveResult> endFunction, System.Action<System.IAsyncResult> endAction, System.Threading.Tasks.Task<System.Net.Sockets.UdpReceiveResult> promise, bool requiresSynchronization)   Unknown
mscorlib.dll!System.Threading.Tasks.TaskFactory<System.Net.Sockets.UdpReceiveResult>.FromAsyncImpl.AnonymousMethod__0(System.IAsyncResult iar)  Unknown
System.dll!System.Net.LazyAsyncResult.Complete(System.IntPtr userToken) Unknown
System.dll!System.Net.ContextAwareResult.CompleteCallback(object state) Unknown
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx)   Unknown
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state) Unknown
System.dll!System.Net.ContextAwareResult.Complete(System.IntPtr userToken)  Unknown
System.dll!System.Net.LazyAsyncResult.ProtectedInvokeCallback(object result, System.IntPtr userToken)   Unknown
System.dll!System.Net.Sockets.BaseOverlappedAsyncResult.CompletionPortCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* nativeOverlapped)  Unknown
mscorlib.dll!System.Threading._IOCompletionCallback.PerformIOCompletionCallback(uint errorCode, uint numBytes, System.Threading.NativeOverlapped* pOVERLAP) Unknown

如果我理解正确,ReceiveAsync 中的错误根源,准确地说是我停止它的方法。但我不知道如何解决它。

我必须做些什么来纠正这个错误?

在 usr 评论后更新:

private async Task<byte[]> Receive(UdpClient client, CancellationToken breakToken)
{
    // Выход из async, если произошёл CancellationRequest
    breakToken.ThrowIfCancellationRequested();

    UdpReceiveResult result;
    try
    {
        result = await client.ReceiveAsync().WithCancellation(breakToken);
    }
    catch(OperationCanceledException)
    {
        // Штатная ситуация ручной остановки Task-а
    }
    catch(ObjectDisposedException) { }

    return result.Buffer;
}

Dispose 调用:

public void Dispose()
{
    this.cancelRecieve?.Cancel();
    this.cancelRecieve?.Dispose();

    try
    {
        this.client?.Close();
    }
    catch(ObjectDisposedException) { }
}

但是catchObjectDisposedException 没有反应。

【问题讨论】:

    标签: c# asynchronous udpclient objectdisposedexception


    【解决方案1】:

    于是,经过近一周的磨难,我找到了原因和解决办法。

    起初,我查看了UdpClient源代码。 ReceiveAsync 方法:

    [HostProtection(ExternalThreading = true)]
    public Task<UdpReceiveResult> ReceiveAsync()
    {
        return Task<UdpReceiveResult>.Factory.FromAsync((callback, state) => BeginReceive(callback, state), (ar)=>
            {
                IPEndPoint remoteEP = null;
                Byte[] buffer = EndReceive(ar, ref remoteEP);
                return new UdpReceiveResult(buffer, remoteEP);
    
            }, null);
    }
    

    第二次,我发现这个帖子有完美的答案:How to abort socket's BeginReceive()?,其中说:

    要取消对 BeginConnect() 方法的挂起调用,请关闭 Socket。当异步操作正在进行时调用 Close() 方法时,将调用提供给 BeginConnect() 方法的回调。对 EndConnect(IAsyncResult) 方法的后续调用将引发 ObjectDisposedException 以指示操作已被取消。

    我们可以看到,原来的ReceiveAsync 方法返回给我们ObjectDisposedException,因为IOOperationClose 调用之后还没有完成。

    为了克服这个问题,我这样做了:

    新的ReceiveAsync实现:

    /// <summary>
    /// Асинхронный запрос на ожидание приёма данных с возможностью досрочного выхода
    /// (для выхода из ожидания вызовите метод Disconnect())
    /// </summary>
    /// <param name="client">Рабочий экземпляр класса UdpClient</param>
    /// <param name="breakToken">Признак досрочного завершения</param>
    /// <returns>Если breakToken произошёл до вызова данного метода или в режиме ожидания
    /// ответа, вернёт пустой UdpReceiveResult; при удачном получении ответа-результат
    /// асинхронной операции чтения</returns>
    public Task<UdpReceiveResult> ReceiveAsync(UdpClient client, CancellationToken breakToken)
        => breakToken.IsCancellationRequested
            ? Task<UdpReceiveResult>.Run(() => new UdpReceiveResult())
            : Task<UdpReceiveResult>.Factory.FromAsync(
                (callback, state) => client.BeginReceive(callback, state),
                (ar) =>
                    {
                        /// Предотвращение <exception cref="ObjectDisposedException"/>
                        if (breakToken.IsCancellationRequested)
                            return new UdpReceiveResult();
    
                        IPEndPoint remoteEP = null;
                        var buffer = client.EndReceive(ar, ref remoteEP);
                        return new UdpReceiveResult(buffer, remoteEP);
                    },
                null);
    

    新的Dispose实现:

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            this.cancelReceive?.Cancel();
            this.client?.Close();
            this.cancelReceive?.Dispose();
        }
    }
    

    我非常希望,我的决定会剥夺其他人我所经历的痛苦。

    【讨论】:

    • 这在所有需要的情况下都无法调用 EndReceive。如果你让它调用 EndReceive,你将再次收到 ObjectDisposedException。但我已经解释了如何处理该异常。除了损坏的异常之外,您的原始代码很好。
    • 文档说必须调用它。不调用它是一个使用错误。实际上,它可能会泄漏内存。捕获异常来处理它是完全没问题的。在这里什么都不做是合适的。
    • 不能保证。它似乎也没有表明没有泄漏。此外,没有理由这样做。新的 ReceiveAsync 代码更加复杂。您所需要的只是捕获(ODE)。
    • 框架是正确的。根据文档,您正在滥用它。
    • 关闭是正确的,但您必须始终调用 End 方法。答案是错误的,原因与我已经解释过的相同:不能保证是这样。答案依赖于反编译 current 代码。随时可能断线。文档状态为The asynchronous BeginReceive operation must be completed by calling the EndReceive method
    【解决方案2】:

    取消挂起接收的唯一方法是像您一样断开/停止/处置。这是对的。您需要捕获并忽略该异常。

    .NET Framework 的一个不幸的设计问题是,这是唯一的解决方法。

    注意,WithCancellation 不会取消 IO。接收仍在运行。这就是为什么必须在 WithCancellation 之后处理套接字以确保没有更多的未决 IO。

    【讨论】:

    • 但是如果我只调用Dispose(delete .WithCancellation(breakToken) in Receive),这个对象将不会被释放。而且我抓不到ObjectDisposedException,我试过了……
    • 不确定你的意思。您拥有的代码很好(假设在超时的情况下您也终止了套接字)。不过,您需要吞下 ObjectDisposedException。
    • 尝试 { 结果 = await client.ReceiveAsync().WithCancellation(breakToken); } catch(OperationCanceledException) { // Штатная ситуация ручной остановки Task-а } catch(ObjectDisposedException) { } 并尝试 { this.client?.Close(); } catch(ObjectDisposedException) { } 没有捕捉到这个异常。
    • 你是在发出 breakToken 的信号吗?在这种情况下,接收任务被丢弃。 Receive 任​​务是 ObjeDispEx 出错的任务。
    • 是的,但是忘记放在这里了)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-04
    • 2017-03-11
    • 2014-06-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多