UdpClient.ReceiveAsync正确提前终止

美好的一天。 我使用UdpClient并使用它包装。

为了阅读我有异步方法:

 private async Task 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 WithCancellation( this Task task, CancellationToken cancellationToken) { var tcs = new TaskCompletionSource(); using (cancellationToken.Register( s => ((TaskCompletionSource)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.FromAsyncCoreLogic(System.IAsyncResult iar, System.Func endFunction, System.Action endAction, System.Threading.Tasks.Task promise, bool requiresSynchronization) Unknown mscorlib.dll!System.Threading.Tasks.TaskFactory.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的错误根,在我的方法中,它是准确的。 但我不知道如何解决它。

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

用户评论后更新:

 private async Task 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) { } } 

但是catch不会对ObjectDisposedException做出反应。

因此,经过近一周的苦难,我找到了原因和解决方案。

首先 ,我查看了UdpClient源代码。 ReceiveAsync方法:

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

第二 ,我发现这篇文章的答案很完美: 如何中止socket的BeginReceive()? ,其中说:

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

而且,正如我们所看到的,原始的ReceiveAsync方法返回ObjectDisposedException ,因为在Close调用之后IOOperation尚未完成。

为了解决这个问题,我这样做了:

新的ReceiveAsync实现:

 ///  /// Асинхронный запрос на ожидание приёма данных с возможностью досрочного выхода /// (для выхода из ожидания вызовите метод Disconnect()) ///  /// Рабочий экземпляр класса UdpClient /// Признак досрочного завершения /// Если breakToken произошёл до вызова данного метода или в режиме ожидания /// ответа, вернёт пустой UdpReceiveResult; при удачном получении ответа-результат /// асинхронной операции чтения public Task ReceiveAsync(UdpClient client, CancellationToken breakToken) => breakToken.IsCancellationRequested ? Task.Run(() => new UdpReceiveResult()) : Task.Factory.FromAsync( (callback, state) => client.BeginReceive(callback, state), (ar) => { /// Предотвращение  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(); } } 

我非常希望,我的决定会剥夺别人的痛苦。

取消待处理接收的唯一方法是断开/停止/处置。 这是对的。 您需要捕获并忽略该exception。

.NET Framework是一个令人遗憾的设计问题,这是唯一的方法。

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