2012-05-21 11 views
6

Tutto, ecco una domanda sulla progettazione/le migliori pratiche di un caso complesso di annullamento Attività: s in C#. Come implementate la cancellazione di un'attività condivisa?Come implementare l'annullamento dell'attività condivisa: s in C#

Come esempio minimo, si assume quanto segue; abbiamo un'operazione "Lavoro" che si può annullare in modo cooperativo e di lunga durata. Accetta un token di cancellazione come argomento e lancia se è stato cancellato. Funziona su alcuni stati dell'applicazione e restituisce un valore. Il suo risultato è richiesto in modo indipendente da due componenti dell'interfaccia utente.

Mentre lo stato dell'applicazione è invariato, il valore della funzione di lavoro deve essere memorizzato nella cache e se un calcolo è in corso, una nuova richiesta non deve iniziare un secondo calcolo, ma piuttosto iniziare ad attendere un risultato.

Uno dei componenti dell'interfaccia utente dovrebbe essere in grado di annullare l'attività senza influire sull'attività dei componenti dell'interfaccia utente.

Sei con me fino ad ora?

Quanto sopra può essere realizzato introducendo una cache delle attività che avvolge la reale attività di lavoro in TaskCompletionSources, le cui attività vengono quindi restituite ai componenti dell'interfaccia utente. Se un componente dell'interfaccia utente annulla la sua attività, abbandona solo TaskCompletionSource Task e non l'attività sottostante. Va tutto bene. I componenti dell'interfaccia utente creano l'origine di annullamento e la richiesta di annullamento è un normale disegno dall'alto verso il basso, con l'attività TaskCompletionSource che collabora in basso.

Ora, per il vero problema. Cosa fare quando lo stato dell'applicazione cambia? Supponiamo che la funzione "Lavoro" funzioni su una copia dello stato non sia fattibile.

Una soluzione sarebbe quella di ascoltare il cambiamento di stato nella cache delle attività (o circa). Se la cache ha un metodo di annullamento utilizzato dall'attività sottostante, quella che esegue la funzione Lavoro, potrebbe annullarla. Ciò potrebbe quindi innescare l'annullamento di tutte le attività TaskCompletionSources allegate: s, e quindi entrambe le componenti dell'interfaccia utente otterrebbero attività annullate. Questa è una sorta di cancellazione dal basso.

C'è un modo preferito per farlo? Esiste uno schema di progettazione che lo descrive in qualche dove?

L'annullamento dal basso verso l'alto può essere implementato, ma si sente un po 'strano. L'UI-task viene creato con un CancellationToken, ma viene cancellato a causa di un altro (Internal) CancellationToken. Inoltre, poiché i token non sono uguali, OperationCancelledException non può essere semplicemente ignorato nell'interfaccia utente - ciò potrebbe (eventualmente) portare a un'eccezione generata nel finalizzatore Task esterno.

+0

Ottima domanda, anche se la mia prima reazione è stata "tl; dr" – dtb

+1

Quindi, per chiarire; si hanno due utenti del risultato dell'attività e si desidera che una sola attività calcoli il valore per stato dell'applicazione; eppure vuoi che ogni consumatore sia in grado di "annullare" in attesa del risultato dell'attività? – Tejs

+0

sì, ma anche che un cambiamento di stato dovrebbe annullare il calcolo. – 4ZM

risposta

1

Sembra che si desidera un set avido di operazioni compito - si dispone di un provider di risultato compito, e poi costruire un compito impostato per restituire la prima operazione è stata completata, es:

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

Ora, mi rifugio' t completamente testato questo, ma dovrebbe consentire di "annullare" l'attesa su un'attività annullando il token (e quindi forzando l'attività "attesa" per completare prima e premere il WaitAny) e consentire di "annullare" l'attività di calcolo .

L'altra cosa è capire un modo pulito per fare in modo che il task "cancella" aspetti senza un blocco orribile. Penso che sia un buon inizio.

+0

Ho usato la classe TaskCompletionSource per fare ciò che fa il tuo consumatore. Ma il problema è lo stesso. Quando viene chiamato OnStateChange e l'attività "reale" viene annullata, la funzione MyApplicationState.GetComputedValue() genererà OpCanceledException. Questo sarà ri-lanciato da Task.WaitAny e quindi 'compito' richiederà particolare attenzione a non saltare in aria nel suo finalizzatore. – 4ZM

+1

Questo è solo se si utilizza 'cts.ThrowIfCancellationIsRequested' - se si controlla semplicemente la condizione nel thread, non viene generata alcuna eccezione se si restituisce semplicemente un valore fittizio. Sei tu stesso a controllare il token, invece che farlo nel framework. In alternativa, puoi semplicemente racchiudere 'Task.WaitAny' con un try/catch interno e gestirne l'eccezione restituendo un valore restituito fittizio" Cancellato "(o qualsiasi altra cosa sia appropriata) – Tejs

+0

Hai ragione. Utilizzare uno speciale valore di ritorno per la cancellazione (invece di usare le eccezioni) è un modo per implementare ciò che voglio. Dovrei introdurre un valore fittizio (o un nuovo tipo) e sembra andare contro l'uso generale dell'annullamento della gestione con eccezioni nel framework .Net Task. Sto tenendo le dita incrociate per una soluzione più ordinata, ma funzionerà. Grazie. – 4ZM

1

Ecco il mio tentativo:

// 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

Questo è qualcosa come quello che ho fatto. Funziona, ma è un buon design? Dal punto di vista dell'interfaccia utente, l'attività viene annullata da qualcun altro. Con questa soluzione, l'interfaccia utente dovrà anche gestire esplicitamente le eccezioni di annullamento dalle attività interne se crea attività di wrapper aggiuntive per evitare che "l'eccezione non osservata sia stata rimandata dal problema del thread di finalizzazione". – 4ZM