cancellationtoken cancel c# multithreading cancellation-token

cancellationtoken c#



¿Cómo hacer un método cancelable sin que se vuelva feo? (8)

Version corta:

Use lock() para sincronizar la llamada Thread.Abort() con las secciones críticas.

Déjame explicar la versión:

Generalmente, al abortar hilos, deberías considerar dos tipos de código:

  • Código de larga ejecución que realmente no te importa si termina
  • Código que absolutamente tiene que correr su curso

El primer tipo es el tipo que no nos importa si el usuario solicitó el aborto. ¿Tal vez estábamos contando hasta cien mil millones y ya no le importa?

Si usáramos centinelas como el CancellationToken , difícilmente los probaríamos en cada iteración del código sin importancia, ¿no?

for(long i = 0; i < bajillion; i++){ if(cancellationToken.IsCancellationRequested) return false; counter++; }

Tan feo. Así que para estos casos, Thread.Abort() es una bendición.

Desafortunadamente, como dicen algunos, no puedes usar Thread.Abort() debido al código atómico que absolutamente tiene que ejecutar. El código ya ha restado dinero de su cuenta, ahora tiene que completar la transacción y transferir el dinero a la cuenta de destino. A nadie le gusta que el dinero desaparezca.

Por suerte, tenemos exclusión mutua para ayudarnos con este tipo de cosas. Y C # lo hace bonito:

//unimportant long task code lock(_lock) { //atomic task code }

Y en otra parte

lock(_lock) //same lock { _thatThread.Abort(); }

El número de bloqueos siempre será <= número de centinelas, porque también querría centinelas en el código sin importancia (para abortar más rápido). Esto hace que el código sea un poco más bonito que la versión centinela, pero también hace que abortar sea mejor, porque no tiene que esperar por cosas sin importancia.

También se debe tener en cuenta que la ThreadAbortException puede ThreadAbortException cualquier lugar, incluso en bloques finally . Esta imprevisibilidad hace que Thread.Abort() tan controvertido.

Con los bloqueos, evitaría esto simplemente bloqueando todo el bloque try-catch-finally . Si finally la limpieza es esencial, se puede bloquear todo el bloque. try bloques de try generalmente se hacen lo más cortos posible, preferiblemente una línea, por lo que no estamos bloqueando ningún código innecesario.

Esto hace que Thread.Abort() , como dices, sea un poco menos malvado. Sin embargo, es posible que no quieras llamarlo en el hilo de la interfaz de usuario, ya que ahora lo estás bloqueando.

Actualmente estoy en el proceso de actualizar nuestros métodos de larga duración para poder cancelarlos. Estoy planeando usar System.Threading.Tasks.CancellationToken para implementar eso.

Nuestros métodos generalmente realizan algunos pasos de larga ejecución (enviar comandos al hardware y luego esperarlos en su mayoría), por ejemplo

void Run() { Step1(); Step2(); Step3(); }

Mi primer pensamiento (tal vez estúpido) sobre la cancelación transformaría esto en

bool Run(CancellationToken cancellationToken) { Step1(cancellationToken); if (cancellationToken.IsCancellationRequested) return false; Step2(cancellationToken); if (cancellationToken.IsCancellationRequested) return false; Step3(cancellationToken); if (cancellationToken.IsCancellationRequested) return false; return true; }

que francamente se ve horrible. Este "patrón" también continuaría dentro de los pasos individuales (y ya son necesariamente bastante largos). Esto haría que Thread.Abort () se vea bastante sexy, aunque sé que no es recomendable.

¿Hay un patrón más limpio para lograr esto que no oculte la lógica de la aplicación debajo de un montón de código repetitivo?

Editar

Como ejemplo de la naturaleza de los pasos, el método Run podría leer

void Run() { GiantRobotor.MoveToBase(); Oven.ThrowBaguetteTowardsBase(); GiantRobotor.CatchBaguette(); // ... }

Estamos controlando diferentes unidades de hardware que necesitan estar sincronizadas para trabajar juntas.


¿Qué pasa con la continuación?

var t = Task.Factory.StartNew(() => Step1(cancellationToken), cancellationToken) .ContinueWith(task => Step2(cancellationToken), cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current) .ContinueWith(task => Step3(cancellationToken), cancellationToken, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current);


Admito que no es bonito, pero la guía es hacer lo que has hecho:

if (cancellationToken.IsCancellationRequested) { /* Stop */ }

... o un poco más corto:

cancellationToken.ThrowIfCancellationRequested()

En general, si puede pasar el token de cancelación a los pasos individuales, puede distribuir los cheques para que no saturen el código. También puede optar por no verificar la cancelación constantemente ; Si las operaciones que está realizando son idempotentes y no requieren muchos recursos, no necesariamente tiene que verificar la cancelación en cada etapa. El momento más importante para verificar es antes de devolver un resultado.

Si está pasando el token a todos sus pasos, podría hacer algo como esto:

public static CancellationToken VerifyNotCancelled(this CancellationToken t) { t.ThrowIfCancellationRequested(); return t; } ... Step1(token.VerifyNotCancelled()); Step2(token.VerifyNotCancelled()); Step3(token.VerifyNotCancelled());


Cuando tuve que hacer algo así, creé un delegado para hacerlo:

bool Run(CancellationToken cancellationToken) { var DoIt = new Func<Action<CancellationToken>,bool>((f) => { f(cancellationToken); return cancellationToken.IsCancellationRequested; }); if (!DoIt(Step1)) return false; if (!DoIt(Step2)) return false; if (!DoIt(Step3)) return false; return true; }

O, si nunca hay ningún código entre los pasos, puede escribir:

return DoIt(Step1) && DoIt(Step2) && DoIt(Step3);


Es posible que desee ver el patrón de NSOperation de Apple como ejemplo. Es más complicado que simplemente hacer que un solo método sea cancelable, pero es bastante robusto.


Estoy un poco sorprendido de que nadie haya propuesto la forma estándar e integrada de manejar esto:

bool Run(CancellationToken cancellationToken) { //cancellationToke.ThrowIfCancellationRequested(); try { Step1(cancellationToken); Step2(cancellationToken); Step3(cancellationToken); } catch(OperationCanceledException ex) { return false; } return true; } void Step1(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ... }

Si bien normalmente no quiere confiar en llevar los cheques a niveles más profundos, en este caso los pasos ya aceptan un CancelToken y deberían tener el cheque de todos modos (como debería hacerlo cualquier método no trivial que acepte un CancelToken).

Esto también le permite ser tan granular o no granular con las comprobaciones necesarias, es decir, antes de operaciones intensivas o de larga ejecución.


Si los pasos son de alguna manera independientes con respecto al flujo de datos dentro del método, pero no se pueden ejecutar de forma paralela, el siguiente enfoque puede ser más legible:

void Run() { // list of actions, defines the order of execution var actions = new List<Action<CancellationToken>>() { ct => Step1(ct), ct => Step2(ct), ct => Step3(ct) }; // execute actions and check for cancellation token foreach(var action in actions) { action(cancellationToken); if (cancellationToken.IsCancellationRequested) return false; } return true; }

Si los pasos no necesitan el token de cancelación porque puede dividirlos en pequeñas unidades, incluso puede escribir una definición de lista más pequeña:

var actions = new List<Action>() { Step1, Step2, Step3 };


Si se permite un refactor, puede refactorizar los métodos del Paso de manera que haya un Step(int number) método Step(int number) .

Luego puede pasar de 1 a N y verificar si el token de cancelación se solicita solo una vez.

bool Run(CancellationToken cancellationToken) { for (int i = 1; i < 3 && !cancellationToken.IsCancellationRequested; i++) Step(i, cancellationToken); return !cancellationToken.IsCancellationRequested; }

O, equivalentemente: (lo que prefieras)

bool Run(CancellationToken cancellationToken) { for (int i = 1; i < 3; i++) { Step(i, cancellationToken); if (cancellationToken.IsCancellationRequested) return false; } return true; }