2012-05-21 9 views
6

Alle, hier ist eine Frage über Design/Best Practices für einen komplexen Fall der Stornierung Aufgabe: s in C#. Wie implementieren Sie die Löschung einer gemeinsamen Aufgabe?So implementieren Sie die Aufhebung der gemeinsamen Aufgabe: s in C#

Nehmen wir als minimales Beispiel Folgendes an: wir haben eine lang laufende, kooperativ kündbare Operation 'Arbeit'. Es akzeptiert ein Abbruch-Token als Argument und löst aus, wenn es abgebrochen wurde. Es arbeitet mit einem bestimmten Anwendungszustand und gibt einen Wert zurück. Ihr Ergebnis wird von zwei UI-Komponenten unabhängig benötigt.

Während die Anwendung Zustand ist unverändert, sollte der Wert der Arbeitsfunktion zwischengespeichert wird, und wenn eine Berechnung nicht abgeschlossen ist, sollte eine neue Anforderung nicht eine zweite Berechnung starten, sondern startet auf ein Ergebnis warten.

Eine der UI-Komponenten sollte in der Lage sein, ihre Aufgabe abzubrechen, ohne dass sich dies auf die anderen Aufgaben der UI-Komponenten auswirkt.

Sind Sie bei mir so weit?

Das Obige kann durch die Einführung eines Task-Cache erreicht werden, die die eigentliche Arbeit Aufgabe in TaskCompletionSources Wraps, deren Aufgabe: s werden dann an die UI-Komponenten zurückgeführt. Wenn eine UI-Komponente ihre Aufgabe abbricht, wird nur die TaskCompletionSource-Aufgabe und nicht die zugrunde liegende Aufgabe aufgegeben. Das ist alles gut. Die UI-Komponenten erstellen die Annullierungsquelle, und die Anforderung zum Abbrechen ist ein normales Top-down-Design, wobei die kooperierende TaskCompletionSource-Aufgabe unten angezeigt wird.

Nun zu dem wirklichen Problem. Was tun, wenn sich der Anwendungsstatus ändert? Nehmen wir an, dass es nicht machbar ist, wenn die Funktion 'Arbeit' auf einer Kopie des Zustands ausgeführt wird.

Eine Lösung wäre, um die Zustandsänderung in der Task-Cache (oder dort etwa) zu hören. Wenn der Cache über ein CancellationToken verfügt, das von der zugrunde liegenden Task verwendet wird, die die Work-Funktion ausführt, kann sie abgebrochen werden. Dies könnte dann eine Annullierung aller angefügten TaskCompletionSources-Tasks auslösen, und somit würden beide UI-Komponenten abgebrochene Tasks erhalten. Dies ist eine Art Bottom-Up-Stornierung.

Gibt es einen bevorzugten Weg, dies zu tun? Gibt es ein Designmuster, das es irgendwo beschreibt?

Die Bottom-Up-Stornierung kann implementiert werden, aber es fühlt sich ein bisschen komisch an. Die UI-Task wird mit einem CancellationToken erstellt, aber aufgrund eines anderen (inneren) CancellationTokens abgebrochen. Auch, da die Token nicht gleich sind, kann die OperationCancelledException nicht nur in der Benutzeroberfläche ignoriert werden - das würde (schließlich) führen zu einer Ausnahme in der äußeren Aufgabe geworfen werden: s Finalizerthread.

+0

Ausgezeichnete Frage, obwohl meine erste Reaktion war "tl; dr" – dtb

+1

Also, zu klären; Sie haben zwei Consumer des Task-Ergebnisses, und Sie möchten nur eine Task den Wert pro Anwendungsstatus berechnen; Sie möchten dennoch, dass jeder Verbraucher "abbrechen" kann, indem er auf das Ergebnis der Aufgabe wartet? – Tejs

+0

ja, aber auch, dass eine Änderung des Zustands die Berechnung abbrechen sollte. – 4ZM

Antwort

1

Es klingt wie Sie einen gierigen Satz Aufgabe Operationen wollen - Sie eine Aufgabe Ergebnis Anbieter haben, und dann eine neue Aufgabe konstruieren stellen Sie die ersten fertig gestellten Betrieb, Ex zurück:

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

Nun, ich haven‘ t diese vollständig getestet, aber es sollte Ihnen ermöglichen, „abbrechen“ auf eine Aufgabe wartet darauf, von den Token-Unterdrückung (und so zwingt die Aufgabe, „warten“ zuerst zu beenden und drücken Sie die WaitAny), und ermöglichen es Ihnen, die Berechnung Aufgabe „cancel“ .

Die andere Sache ist es, eine saubere Art und Weise, um herauszufinden, macht die „Abbrechen“ Aufgabe warten, ohne schreckliche Blockierung. Es ist ein guter Anfang, denke ich.

+0

Ich habe die Klasse TaskCompletionSource verwendet, um das zu tun, was Ihr Kunde tut. Aber das Problem ist das gleiche. Wenn das OnStateChange aufgerufen wird und wenn die "echte" Aufgabe abgebrochen wird, wird die MyApplicationState.GetComputedValue() - Funktion OpCanceledException werfen. Dies wird von Task.WaitAny erneut geworfen und dann wird 'Aufgabe' besondere Sorgfalt darauf verwenden, nicht in seinem Finalizer zu explodieren. – 4ZM

+1

Das ist nur, wenn Sie 'cts.ThrowIfCancellationIsRequested' verwenden - wenn Sie einfach nach der Bedingung im Thread suchen, wird keine Ausnahme ausgelöst, wenn Sie einfach einen Dummy-Wert zurückgeben. Sie überprüfen das Token selbst, anstatt es zu tun. Alternativ können Sie einfach den Task.WaitAny mit einem internen try/catch umbrechen und die Ausnahme behandeln, indem Sie einen "Canceled" Dummy-Rückgabewert (oder was auch immer) – Tejs

+0

zurückgeben. Sie haben Recht. Die Verwendung eines speziellen Rückgabewerts für die Stornierung (anstelle von Ausnahmen) ist eine Möglichkeit, das zu implementieren, was ich möchte. Ich würde einen Dummy-Wert (oder einen neuen Typ) einführen müssen, und es scheint, gegen die allgemeine Verwendung der Behandlung von Annullierung mit Ausnahmen im .Net Tasks Rahmen jedoch zu gehen. Ich drücke die Daumen für eine bessere Lösung, aber das wird funktionieren. Vielen Dank. – 4ZM

1

Hier ist mein Versuch:

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

mit

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

Das ist so etwas wie das, was ich gemacht habe. Es funktioniert, aber ist es ein gutes Design? Aus Sicht der Benutzer wird die Aufgabe von einem anderen Benutzer abgebrochen. Bei dieser Lösung muss die Benutzeroberfläche auch explizit die Abbruchausnahmen von inneren Aufgaben behandeln, wenn zusätzliche Wrapper-Aufgaben erstellt werden, um das Problem zu vermeiden, dass die unbeobachtete Ausnahme vom Finalizer-Thread erneut ausgelöst wurde. – 4ZM