Las operaciones de red asincrónicas nunca terminan

Tengo varias operaciones de red asíncronas que devuelven una tarea que puede que nunca termine:

  1. UdpClient.ReceiveAsync no acepta un CancellationToken
  2. TcpClient.GetStream devuelve un NetworkStream que no respeta el CancellationToken en Stream.ReadAsync (verificando la cancelación solo al comienzo de la operación)

Ambos esperan un mensaje que quizás nunca llegue (debido a la pérdida de paquetes o a la falta de respuesta, por ejemplo). Eso significa que tengo tareas fantasmas que nunca se completan, las que nunca se ejecutarán y las tomas en espera. Sé que puedo usar TimeoutAfter , pero eso solo solucionará el problema de continuación.

Entonces, ¿qué se supone que debo hacer?

Así que hice un método de extensión en IDisposable que crea un CancellationToken que elimina la conexión en tiempo de espera, por lo que la tarea finaliza y todo continúa:

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

Y el uso es extremadamente simple:

 try { var client = new UdpClient(); using (client.CreateTimeoutScope(TimeSpan.FromSeconds(2))) { var result = await client.ReceiveAsync(); // Handle result } } catch (ObjectDisposedException) { return null; } 

Información extra:

 public sealed class DisposableScope : IDisposable { private readonly Action _closeScopeAction; public DisposableScope(Action closeScopeAction) { _closeScopeAction = closeScopeAction; } public void Dispose() { _closeScopeAction(); } } 

Entonces, ¿qué se supone que debo hacer?

En este caso particular, preferiría usar UdpClient.Client.ReceiveTimeout y TcpClient.ReceiveTimeout para TcpClient.ReceiveTimeout tiempo de espera de una operación de recepción UDP o TCP. Me gustaría que el error de tiempo de espera provenga del socket, en lugar de hacerlo desde cualquier fuente externa.

Si además de eso necesito observar algún otro evento de cancelación, como un clic en el botón de la interfaz de usuario, simplemente usaría WithCancellation de “¿Cómo cancelo las operaciones de WithCancellation no WithCancellation de Stephen Toub ?” , Me gusta esto:

 using (var client = new UdpClient()) { UdpClient.Client.ReceiveTimeout = 2000; var result = await client.ReceiveAsync().WithCancellation(userToken); // ... } 

Para abordar el comentario , en caso de que ReceiveTimeout no tenga efecto en ReceiveAsync , igual usaría 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); // ... } 

OMI, esto muestra más claramente mis intenciones como desarrollador y es más legible para un tercero. Además, no es necesario atrapar la ObjectDisposedException ObjectDisposedException. Todavía tengo que observar OperationCanceledException en algún lugar de mi código de cliente que llama a esto, pero estaría haciendo eso de todos modos. OperationCanceledException generalmente se destaca de otras excepciones, y tengo la opción de verificar OperationCanceledException.CancellationToken para observar el motivo de la cancelación.

Aparte de eso, no hay mucha diferencia con la respuesta de @I3arnon. Simplemente no siento que necesite otro patrón para esto, ya que tengo WithCancellation a mi disposición.

Para abordar los comentarios:

  • Solo capturaría OperationCanceledException en el código del cliente, es decir:
 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); } } 
  • Sí, WithCancellation con cada llamada a ReadAsync y me gusta ese hecho por los siguientes motivos. En primer lugar, puedo crear una extensión ReceiveAsyncWithToken :
 public static class UdpClientExt { public static Task ReceiveAsyncWithToken( this UdpClient client, CancellationToken token) { return client.ReceiveAsync().WithCancellation(token); } } 

En segundo lugar, en 3 años a partir de ahora puedo estar revisando este código para .NET 6.0. Para entonces, Microsoft puede tener una nueva API, UdpClient.ReceiveAsyncWithTimeout . En mi caso, simplemente reemplazaré ReceiveAsyncWithToken(token) o ReceiveAsync().WithCancellation(token) con ReceiveAsyncWithTimeout(timeout, userToken) . No sería tan obvio tratar con CreateTimeoutScope .