2012-05-21 13 views
6

Todas, aquí hay una pregunta sobre el diseño/las mejores prácticas de un caso complejo de cancelación Tarea: s en C#. ¿Cómo se implementa la cancelación de una tarea compartida?Cómo implementar la cancelación de tareas compartidas: s en C#

Como un ejemplo mínimo, supongamos lo siguiente; tenemos una operación de larga duración, cooperativamente cancelable 'Trabajo'. Acepta un token de cancelación como argumento y tira si se ha cancelado. Funciona en algún estado de aplicación y devuelve un valor. Su resultado es requerido independientemente por dos componentes UI.

Si bien el estado de la aplicación no se modifica, el valor de la función Work debe almacenarse en caché, y si un cálculo está en curso, una nueva solicitud no debe iniciar un segundo cálculo sino comenzar a esperar un resultado.

Cualquiera de los componentes de la interfaz de usuario debería poder cancelar su tarea sin afectar la otra tarea de los componentes de la interfaz de usuario.

¿Estás conmigo hasta ahora?

Lo anterior se puede lograr mediante la introducción de un caché de tareas que envuelve la tarea de trabajo real en TaskCompletionSources, cuya tarea: s se devuelve a los componentes de la interfaz de usuario. Si un componente de la interfaz de usuario cancela su tarea, solo abandona la tarea TaskCompletionSource y no la tarea subyacente. Esto está todo bien. Los componentes de UI crean CanceSource y la solicitud para cancelar es un diseño descendente normal, con TaskCompletionSource Tarea que coopera en la parte inferior.

Ahora, al problema real. ¿Qué hacer cuando cambia el estado de la aplicación? Supongamos que hacer funcionar la función 'Trabajo' en una copia del estado no es factible.

Una solución sería escuchar el cambio de estado en la memoria caché de la tarea (o de allí). Si el caché tiene un CancelToken utilizado por la tarea subyacente, el que ejecuta la función Work, podría cancelarlo. Esto podría desencadenar una cancelación de todas las TaskCompletionSources Task: s adjuntas, y así ambos componentes de la interfaz de usuario obtendrían tareas canceladas. Este es un tipo de cancelación de abajo hacia arriba.

¿Existe alguna forma de hacerlo? ¿Hay un patrón de diseño que lo describa en algún lugar?

Se puede implementar la cancelación de abajo hacia arriba, pero se siente un poco raro. La tarea de UI se crea con un CancellationToken, pero se cancela debido a otro CancellationToken (interno). Además, dado que los tokens no son lo mismo, OperationCancelledException no puede ignorarse en la IU, lo que (eventualmente) llevaría a que se genere una excepción en el finalizador de la Tarea externa.

+0

excelente pregunta, aunque mi primera reacción fue "tl; dr" – dtb

+1

Por lo tanto, para aclarar; tiene dos consumidores del resultado de la tarea y solo desea que una tarea calcule el valor por estado de la aplicación; sin embargo, ¿desea que cada consumidor pueda "cancelar" la espera del resultado de la tarea? – Tejs

+0

sí, pero también que un cambio de estado debe cancelar el cálculo. – 4ZM

Respuesta

1

Parece que usted quiere un conjunto codiciosos de operaciones de la tarea - usted tiene un proveedor resultado de tareas, y luego construye un objeto definido para devolver la primera operación se ha completado, por ejemplo:

// Task Provider - basically, construct your first call as appropriate, and then 
// invoke this on state change 

public void OnStateChanged() 
{ 
    if(_cts != null) 
     _cts.Cancel(); 

    _cts = new CancellationTokenSource(); 
    _task = Task.Factory.StartNew(() => 
     { 
      // Do Computation, checking for cts.IsCancellationRequested, etc 
      return result; 
     }); 
} 

// Consumer 1 

var cts = new CancellationTokenSource(); 
var task = Task.Factory.StartNew(() => 
    { 
     var waitForResultTask = Task.Factory.StartNew(() => 
      { 
       // Internally, this is invoking the task and waiting for it's value 
       return MyApplicationState.GetComputedValue(); 
      }); 

     // Note this task cares about being cancelled, not the one above 
     var cancelWaitTask = Task.Factory.StartNew(() => 
     { 
       while(!cts.IsCancellationRequested) 
       Thread.Sleep(25); 

       return someDummyValue; 
     }); 

     Task.WaitAny(waitForResultTask, cancelWaitTask); 

     if(cancelWaitTask.IsComplete) 
      return "Blah"; // I cancelled waiting on the original task, even though it is still waiting for it's response 
     else 
      return waitForResultTask.Result; 
    }); 

Ahora, no he t probado completamente esto, pero debería permitirle "cancelar" la espera en una tarea cancelando el token (y forzando así a que la tarea "esperar" se complete primero y presione WaitAny), y le permita "cancelar" la tarea de cálculo .

La otra cosa es encontrar una manera limpia de hacer que la tarea "cancelar" espere sin bloqueos horribles. Es un buen comienzo, creo.

+0

He utilizado la claseTaskCompletionSource para hacer lo que hace su consumidor. Pero el problema es el mismo. Cuando se llama al OnStateChange y se cancela la tarea "real", la función MyApplicationState.GetComputedValue() lanzará OpCanceledException. Esto será devuelto por Task.WaitAny y luego 'task' requerirá un cuidado especial para no explotar en su finalizador. – 4ZM

+1

Eso es solo si usa 'cts.ThrowIfCancellationIsRequested'; si simplemente verifica la condición en el hilo, no se lanza ninguna excepción si simplemente devuelve un valor ficticio. Usted verifica la ficha usted mismo, en lugar del marco que lo hace. Alternativamente, puede simplemente ajustar el 'Task.WaitAny' con un try/catch interno y manejar la excepción al devolver un valor de retorno ficticio" Cancelado "(o lo que sea apropiado) – Tejs

+0

Tiene razón. El uso de un valor de retorno especial para la cancelación (en lugar de usar excepciones) es una forma de implementar lo que quiero. Tendría que introducir un valor ficticio (o un nuevo tipo) y parece ir en contra del uso general de la cancelación de manejo con excepciones en el marco de .Net Tasks. Mantengo los dedos cruzados para obtener una solución más ordenada, pero esto funcionará. Gracias. – 4ZM

1

Aquí está mi intento:

// the Task for the current application state 
Task<Result> _task; 
// a CancellationTokenSource for the current application state 
CancellationTokenSource _cts; 

// called when the application state changes 
void OnStateChange() 
{ 
    // cancel the Task for the old application state 
    if (_cts != null) 
    { 
     _cts.Cancel(); 
    } 

    // new CancellationTokenSource for the new application state 
    _cts = new CancellationTokenSource(); 
    // start the Task for the new application state 
    _task = Task.Factory.StartNew<Result>(() => { ... }, _cts.Token); 
} 

// called by UI component 
Task<Result> ComputeResultAsync(CancellationToken cancellationToken) 
{ 
    var task = _task; 
    if (cancellationToken.CanBeCanceled && !task.IsCompleted) 
    { 
     task = WrapTaskForCancellation(cancellationToken, task); 
    } 
    return task; 
} 

con

static Task<T> WrapTaskForCancellation<T>(
    CancellationToken cancellationToken, Task<T> task) 
{ 
    var tcs = new TaskCompletionSource<T>(); 
    if (cancellationToken.IsCancellationRequested) 
    { 
     tcs.TrySetCanceled(); 
    } 
    else 
    { 
     cancellationToken.Register(() => 
     { 
      tcs.TrySetCanceled(); 
     }); 
     task.ContinueWith(antecedent => 
     { 
      if (antecedent.IsFaulted) 
      { 
       tcs.TrySetException(antecedent.Exception.GetBaseException()); 
      } 
      else if (antecedent.IsCanceled) 
      { 
       tcs.TrySetCanceled(); 
      } 
      else 
      { 
       tcs.TrySetResult(antecedent.Result); 
      } 
     }, TaskContinuationOptions.ExecuteSynchronously); 
    } 
    return tcs.Task; 
} 
+0

Esto es algo así como lo que hice. Funciona, pero ¿es un buen diseño? Desde la perspectiva de UI, la tarea es cancelada por alguien más. Con esta solución, la IU también deberá manejar explícitamente excepciones de cancelación de tareas internas si crea tareas de contenedor adicionales para evitar el problema de "la excepción no observada fue reemplazada por el hilo del finalizador". – 4ZM

Cuestiones relacionadas