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.ObjectDisposedException
。 CallStack
:
> 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。