source run net example cancellationtoken cancel async c# .net task-parallel-library idisposable cancellation-token

c# - run - ¿Por qué existe CancelTokenRegistration y por qué implementa IDisposable?



cancellationtoken timeout (2)

He estado viendo un código que utiliza Cancellation.Register con una cláusula de using en el resultado de CancellationTokenRegistration Registro:

using (CancellationTokenRegistration ctr = token.Register(() => wc.CancelAsync())) { await wc.DownloadStringAsync(new Uri("http://www.hamster.com")); }

Entiendo que debe asegurarse de que IDisposable un IDisposable , pero ¿por qué incluso implementa IDisposable ? ¿Qué recursos tiene que liberar? Los únicos métodos que tiene tienen en cuenta la igualdad.

¿Qué pasa si no lo Dispose ? que fugas


¿Por qué incluso implementa IDisposable? ¿Qué recursos tiene que liberar?

IDisposable se usa a menudo para cosas que no están relacionadas con la liberación de recursos; es una forma conveniente de garantizar que se hará algo al final de un bloque de using , incluso si se produce una excepción. Algunas personas (no yo) argumentan que hacer esto es un abuso del patrón de Dispose .

En el caso de CancellationToken.Register , el "algo" es la CancellationToken.Register del registro de la devolución de llamada.

Tenga en cuenta que en el código que publicó en su pregunta, el uso de un bloque de using en el registro de cancelación cancelado es casi seguro que es un error: dado que wc.DownloadStringAsync regresa inmediatamente, la devolución de llamada se cancelará inmediatamente, antes de que la operación tenga la oportunidad de cancelarse. por lo wc.CancelAsync , nunca se llamará a wc.CancelAsync , incluso si se señala el CancellationToken . Tendría sentido si se wc.DownloadStringAsync la llamada a wc.DownloadStringAsync , porque en ese caso no se alcanzaría el final del bloque de using antes de completar wc.DownloadStringAsync . (EDIT: ya no es cierto desde que se editó la pregunta)

¿Qué pasa si no lo desechas? que fugas

En este caso, lo que sucede es que la devolución de llamada no está desregistrada. Es probable que realmente no importe, ya que solo se hace referencia al token de cancelación, y dado que CancellationToken es un tipo de valor que normalmente se almacena solo en la pila, la referencia desaparecerá cuando quede fuera del alcance. Por lo tanto, no filtrará nada en la mayoría de los casos, pero podría hacerlo si almacena el CancellationToken algún lugar del montón. EDIT: en realidad, eso no es correcto; Vea la respuesta de Noseratio para una explicación.


Este patrón es una forma conveniente de asegurarse de que se llame automáticamente a CancellationTokenRegistration.Unregister() . Stephen Toub lo usa a menudo en sus publicaciones de programación paralela con .NET , por ejemplo, here .

Entiendo que debe asegurarse de que dispone de un IDisposable, pero ¿por qué incluso implementa IDisposable? ¿Qué recursos tiene que liberar? Los únicos métodos que tiene tienen en cuenta la igualdad.

En mi opinión, la mejor respuesta a esto se puede encontrar en la publicación del Marco de cancelación de .NET 4 de Mike Liddell de Microsoft:

Cuando se registra una devolución de llamada en un CancellationToken , se captura el ExecutionContext del subproceso actual para que la devolución de llamada se ejecute con el mismo contexto de seguridad. La captura del contexto de sincronización del hilo actual es opcional, se puede solicitar a través de una sobrecarga de ct.Register() si es necesario. Las devoluciones de llamada normalmente se almacenan y luego se ejecutan cuando se solicita la cancelación, pero si se registra una devolución de llamada después de que se haya solicitado la cancelación, la devolución de llamada se ejecutará inmediatamente en el hilo actual, o mediante Send() en el SynchronizationContext actual, si corresponde.

Cuando se registra una devolución de llamada en un CancellationToken , el objeto devuelto es un registro CancellationToken CancellationTokenRegistration . Este es un tipo de estructura de luz que es IDiposable y la eliminación de este objeto de registro hace que la devolución de llamada se cancele. Se garantiza que después de que el método Dispose() haya regresado, la devolución de llamada registrada no se está ejecutando ni comenzará posteriormente. Una consecuencia de esto es que CancellationTokenRegistration.Dispose() debe bloquear si la devolución de llamada se está ejecutando actualmente. Por lo tanto, todas las devoluciones de llamadas registradas deben ser rápidas y no bloquearse por ninguna duración significativa.

Otro documento relevante de Mike Liddell es "Uso del soporte de cancelación en .NET Framework 4" (UsingCancellationinNET4.pdf) .

Actualizado , esto es verificable aquí en la Fuente de Referencia .

También es importante tener en cuenta que la devolución de llamada de cancelación se registra con el CancellationTokenSource , no con el de CancellationToken . Por lo tanto, si CancellationTokenRegistration.Dispose() no está correctamente en el ámbito, el registro permanecerá activo durante toda la vida útil del objeto principal CancellationTokenSource . Esto puede provocar una devolución de llamada inesperada cuando finalice el alcance de la operación asíncrona, por ejemplo:

async Task TestAsync(WebClient wc, CancellationToken token) { token.Register(() => wc.CancelAsync()); await wc.DownloadStringAsync(new Uri("http://www.hamster.com")); } // CancellationTokenSource.Cancel() may still get called later, // in which case wc.CancelAsync() will be invoked too

Por lo tanto, es importante tener en cuenta el CancellationTokenRegistration Registro desechable con el using (o llamar al CancellationTokenRegistration.Dispose() explícitamente con try/finally ).