¿Por qué CancellationToken es independiente de CancellationTokenSource?


137

Estoy buscando una razón de por qué CancellationTokense introdujo .NET struct además de la CancellationTokenSourceclase. Entiendo cómo se utilizará la API, pero también quiero entender por qué está diseñada de esa manera.

Es decir, ¿por qué tenemos:

var cts = new CancellationTokenSource();
SomeCancellableOperation(cts.Token);

...
public void SomeCancellableOperation(CancellationToken token) {
    ...
    token.ThrowIfCancellationRequested();
    ...
}

en lugar de pasar directamente CancellationTokenSourcecomo:

var cts = new CancellationTokenSource();
SomeCancellableOperation(cts);

...
public void SomeCancellableOperation(CancellationTokenSource cts) {
    ...
    cts.ThrowIfCancellationRequested();
    ...
}

¿Es esta una optimización del rendimiento basada en el hecho de que las verificaciones del estado de cancelación ocurren con más frecuencia que pasar el token?

Entonces, ¿ CancellationTokenSourcepuede realizar un seguimiento y actualizar CancellationTokens, y para cada token la verificación de cancelación es un acceso de campo local?

Dado que un bool volátil sin bloqueo es suficiente en ambos casos, todavía no puedo ver por qué eso sería más rápido.

¡Gracias!

Respuestas:


110

Estuve involucrado en el diseño e implementación de estas clases.

La respuesta corta es " separación de preocupaciones ". Es bastante cierto que existen diversas estrategias de implementación y que algunas son más simples, al menos en lo que respecta al sistema de tipos y al aprendizaje inicial. Sin embargo, CTS y CT están diseñados para su uso en muchos escenarios (como pilas de bibliotecas profundas, cómputo paralelo, asíncrono, etc.) y, por lo tanto, se diseñaron teniendo en cuenta muchos casos de uso complejos. Es un diseño destinado a fomentar patrones exitosos y desalentar los antipatrones sin sacrificar el rendimiento.

Si se dejaba la puerta abierta por mal comportamiento de las API, entonces la utilidad del diseño de cancelación podría erosionarse rápidamente.

CancellationTokenSource == "disparador de cancelación", además genera oyentes vinculados

CancellationToken == "oyente de cancelación"


77
¡Muchas gracias! El conocimiento interno es realmente apreciado.
Andrey Tarantsov

stackoverflow.com/questions/39077497/… ¿Tiene idea de cuál debería ser el tiempo de espera predeterminado para inputstream del StreamSocket? Cuando uso cancellationtoken para cancelar la operación de lectura, también cierra el socket en cuestión. ¿Puede haber alguna forma de superar este problema?
sam18

@ Mike - Solo curiosidad: ¿por qué no puedes llamar a algo como ThrowIfCancellationRequested () en un CTS como puedes en un CT?
rory.ap

Tienen diferentes responsabilidades y no es demasiado detallado escribir cts.Token.ThrowIfCancellationRequested (), por lo que no agregamos la API de escucha directamente en CTS.
Mike Liddell

@ Mike ¿Por qué cancellationtoken es una estructura y no una clase? No veo ninguna ventaja de rendimiento
Neir0

85

Tenía la pregunta exacta y quería entender la lógica detrás de este diseño.

La respuesta aceptada obtuvo la justificación correcta. Aquí está la confirmación del equipo que diseñó esta función (el énfasis es mío):

Dos nuevos tipos forman la base del marco: A CancellationTokenes una estructura que representa una 'solicitud potencial de cancelación'. Esta estructura se pasa a las llamadas a métodos como un parámetro y el método puede sondearla o registrar una devolución de llamada para que se active cuando se solicita la cancelación. A CancellationTokenSourcees una clase que proporciona el mecanismo para iniciar una solicitud de cancelación y tiene una Token propiedad para obtener un token asociado. Hubiera sido natural combinar estas dos clases en una sola, pero este diseño permite que las dos operaciones clave (iniciar una solicitud de cancelación versus observar y responder a la cancelación) se separen limpiamente. En particular, los métodos que solo toman una CancellationTokenpueden observar una solicitud de cancelación pero no pueden iniciarla.

Enlace: Marco de cancelación de .NET 4

En mi opinión, el hecho de que CancellationTokensolo pueda observar el estado y no cambiarlo es extremadamente crítico. Puede repartir la ficha como un caramelo y nunca preocuparse de que otra persona, aparte de usted, la cancele. Te protege del código hostil de terceros. Sí, las posibilidades son escasas, pero personalmente me gusta esa garantía.

También siento que hace que la API sea más limpia y evita errores accidentales y promueve un mejor diseño de componentes.

Veamos la API pública para ambas clases.

API de cancelación de token

API CancellationTokenSource

Si los combinara, al escribir LongRunningFunction, veré métodos como esas sobrecargas múltiples de 'Cancelar' que no debería usar. Personalmente, odio ver el método Dispose también.

Creo que el diseño de clase actual sigue la filosofía del 'pozo de éxito', guía a los desarrolladores para crear mejores componentes que puedan manejar la Taskcancelación y luego instrumentarlos juntos de numerosas maneras para crear flujos de trabajo complicados.

Déjame hacerte una pregunta, ¿te has preguntado cuál es el propósito del token? No tenía sentido para mí. Y luego leí Cancelación en hilos gestionados y todo se volvió claro como el cristal.

Creo que el diseño del marco de cancelación en TPL es absolutamente de primera categoría.


2
Si se pregunta cómo CancellationTokenSourcepuede realmente iniciar la solicitud de cancelación en su token asociado (el token no puede hacerlo solo): CancellationToken tiene este constructor interno: internal CancellationToken(CancellationTokenSource source) { this.m_source = source; }y esta propiedad: public bool IsCancellationRequested { get { return this.m_source != null && this.m_source.IsCancellationRequested; } }CancellationTokenSource usa el constructor interno, por lo que el token tiene referencia al fuente (m_source)
chviLadislav

65

Están separados no por razones técnicas sino semánticas. Si observa la implementación de CancellationTokenbajo ILSpy, encontrará que es simplemente una envoltura CancellationTokenSource(y, por lo tanto, no es diferente en términos de rendimiento que pasar una referencia).

Proporcionan esta separación de funcionalidad para hacer que las cosas sean más predecibles: cuando pasa un método a CancellationToken, sabe que sigue siendo el único que puede cancelarlo. Claro, el método aún podría arrojar un TaskCancelledException, pero el CancellationTokenmismo, y cualquier otro método que haga referencia al mismo token, permanecería seguro.


¡Gracias! Mi reacción de parpadeo es que, semánticamente, es un enfoque cuestionable, a la par que proporcionar IReadOnlyList. Sin embargo, suena muy plausible, así que acepta tu respuesta.
Andrey Tarantsov

1
Después de pensarlo más, me encuentro cuestionando la plausibilidad. ¿No tendría más sentido, entonces, proporcionar la interfaz ICancellationToken que CancellationTokenSource implementaría? Ojalá alguien del equipo de .NET
interviniera

Eso todavía podría dar lugar a un programador astuto que lo envíe CancellationTokenSource. Pensarías que podrías decir "no hagas eso", pero la gente (¡incluyéndome a mí!) De vez en cuando hace estas cosas para obtener alguna funcionalidad oculta, y sucedería. Esa es mi teoría actual, al menos.
Cory Nelson el

1
No es así como diseñas las API en la mayoría de los casos, a menos que esté relacionado con la seguridad. Prefiero apostar por alguna otra explicación, como "mantener sus opciones abiertas para optimizaciones de rendimiento en el futuro". Pero aún así, el tuyo es el mejor hasta ahora.
Andrey Tarantsov

Oye, espero que me disculpe por reasignar la respuesta aceptada a la persona involucrada en la implementación. Si bien ambos dicen lo mismo, creo que SO se beneficia de que la respuesta definitiva esté en la cima.
Andrey Tarantsov

10

El CancellationTokenes una estructura tantas copias podrían existir debido a pasarlo a métodos.

Los CancellationTokenSourceconjuntos del estado de todos los ejemplares de una muestra cuando se llama Cancela la fuente. Ver esta página de MSDN

La razón del diseño podría ser solo una cuestión de separación de preocupaciones y la velocidad de una estructura.


1
Gracias. Tenga en cuenta que otra respuesta dice que técnicamente (en IL), CancellationTokenSource no establece el estado de los tokens; en cambio, los tokens envuelven una referencia real a CancellationTokenSource y simplemente acceden a ella para verificar la cancelación. En todo caso, la velocidad solo se puede perder aquí.
Andrey Tarantsov

+1. No entiendo. si es un tipo de valor, entonces cada método tiene su propio valor (valor separado). Entonces, ¿cómo sabe un método si se ha llamado a cancelar? NO tiene ninguna referencia a TokenSource. todo lo que el método puede ver es un tipo de valor local como "5". puedes explicar ?
Royi Namir

1
@RoyiNamir: Cada CancellationToken tiene una referencia privada "m_source" del tipo CancellationTokenSource
Andreyul

2

El CancellationTokenSourcees la "cosa" que emite la cancelación, por cualquier razón. Necesita una forma de "enviar" esa cancelación a todos los CancellationTokenque ha emitido. Así es como, por ejemplo, ASP.NET puede cancelar operaciones cuando se cancela una solicitud. Cada solicitud tiene un CancellationTokenSourceque reenvía la cancelación a todos los tokens que ha emitido.

Esto es genial para pruebas unitarias, por cierto: cree su propio token de cancelación, obtenga un token, llame Cancelal origen y pase el token a su código que tiene que manejar la cancelación.


Gracias, pero ambas responsabilidades (que están muy relacionadas) podrían asignarse a la misma clase. Realmente no explica por qué están separados.
Andrey Tarantsov
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.