2014-06-23 26 views
16

Normalerweise poste ich keine Frage mit der Antwort, aber dieses Mal möchte ich einige Aufmerksamkeit auf etwas lenken, was ich denke, ein unklares aber allgemeines Problem sein könnte. Es wurde von this question ausgelöst, seitdem habe ich meinen eigenen alten Code überprüft und festgestellt, dass davon auch einiges betroffen war.Faulted vs Canceled Aufgabenstatus nach CancellationToken.ThrowIfCancellationRequested

Der folgende Code beginnt und wartet auf zwei Aufgaben, task1 und task2, die fast identisch sind. task1 unterscheidet sich nur von task2 darin, dass es eine nie endende Schleife ausführt. IMO, beide Fälle sind ziemlich typisch für einige reale Szenarien, die CPU-gebundene Arbeit ausführen.

using System; 
using System.Threading; 
using System.Threading.Tasks; 

namespace ConsoleApplication 
{ 
    public class Program 
    { 
     static async Task TestAsync() 
     { 
      var ct = new CancellationTokenSource(millisecondsDelay: 1000); 
      var token = ct.Token; 

      // start task1 
      var task1 = Task.Run(() => 
      { 
       for (var i = 0; ; i++) 
       { 
        Thread.Sleep(i); // simulate work item #i 
        token.ThrowIfCancellationRequested(); 
       } 
      }); 

      // start task2 
      var task2 = Task.Run(() => 
      { 
       for (var i = 0; i < 1000; i++) 
       { 
        Thread.Sleep(i); // simulate work item #i 
        token.ThrowIfCancellationRequested(); 
       } 
      }); 

      // await task1 
      try 
      { 
       await task1; 
      } 
      catch (Exception ex) 
      { 
       Console.WriteLine(new { task = "task1", ex.Message, task1.Status }); 
      } 

      // await task2 
      try 
      { 
       await task2; 
      } 
      catch (Exception ex) 
      { 
       Console.WriteLine(new { task = "task2", ex.Message, task2.Status }); 
      } 
     } 

     public static void Main(string[] args) 
     { 
      TestAsync().Wait(); 
      Console.WriteLine("Enter to exit..."); 
      Console.ReadLine(); 
     } 
    } 
} 

Die Geige is here. Der Ausgang:

 
{ task = task1, Message = The operation was canceled., Status = Canceled } 
{ task = task2, Message = The operation was canceled., Status = Faulted } 

Warum der Status task1Cancelled ist, aber der Status von task2 ist Faulted? Hinweis, in beiden Fällen tue ich nicht pass token als 2. Parameter zu Task.Run.

+1

Ich bin froh:

Der richtige Code für task1 sollte diese sein. – i3arnon

+1

@ l3arnon, es war in der Tat ein großer Beitrag, beide tatsächlich. – Noseratio

Antwort

10

Hier gibt es zwei Probleme. Erstens ist es immer eine gute Idee, CancellationToken an die API Task.Run zu übergeben und sie zusätzlich dem Lambda der Aufgabe zur Verfügung zu stellen. Dadurch wird das Token mit der Aufgabe verknüpft und ist für die korrekte Weiterleitung der durch token.ThrowIfCancellationRequested ausgelösten Stornierung unerlässlich.

Dies gilt jedoch nicht erklären, warum der Stornostatus für task1 noch richtig propagiert wird (task1.Status == TaskStatus.Canceled), während es nicht tut task2 (task2.Status == TaskStatus.Faulted).

Nun könnte dies einer der sehr seltenen Fälle sein, in denen die clevere Inferenzlogik vom Typ C# gegen den Willen des Entwicklers spielen kann. Es ist in großen Details here und here diskutiert. Um es zusammenzufassen, falls mit task1, die folgende Überschreibung von Task.Run wird durch Compiler abgeleitet:

public static Task Run(Func<Task> function) 

statt:

public static Task Run(Action action) 

Das ist, weil die task1 Lambda keinen natürlichen Codepfad hat aus dem for Schleife, so kann es auch ein Func<Task> Lambda sein, trotz es ist nicht async und es gibt nichts zurück. Dies ist die Option, die der Compiler mehr als Action bevorzugt. Dann ist die Verwendung einer solchen Überschreibung von Task.Run entspricht dies:

var task1 = Task.Factory.StartNew(new Func<Task>(() => 
{ 
    for (var i = 0; ; i++) 
    { 
     Thread.Sleep(i); // simulate work item #i 
     token.ThrowIfCancellationRequested(); 
    } 
})).Unwrap(); 

Eine verschachtelte Aufgabe vom Typ Task<Task> zurückgegeben durch Task.Factory.StartNew, die unwrapped zu Task von Unwrap() erhält. Task.Runis smart enough, um eine solche Entpackung automatisch durchzuführen, wenn sie Func<Task> akzeptiert. Die unverpackte Versprechen-style-Aufgabe propagiert den Löschstatus korrekt von seiner inneren Aufgabe, die als OperationCanceledException Ausnahme von Func<Task> Lambda ausgelöst wird. Dies passiert nicht für task2, die ein Action Lambda akzeptiert und keine inneren Aufgaben erstellt.Die Annullierung wird nicht für task2 propagiert, da token nicht mit task2 über Task.Run verknüpft wurde.

Am Ende kann dies ein gewünschtes Verhalten für task1 sein (sicherlich nicht für task2), aber wir wollen in beiden Fällen keine verschachtelten Aufgaben hinter der Szene erstellen. Darüber hinaus kann dieses Verhalten für task1 leicht durch die Einführung einer bedingten break aus der for-Schleife gebrochen werden. meine Beiträge provozierten Fragen

var task1 = Task.Run(new Action(() => 
{ 
    for (var i = 0; ; i++) 
    { 
     Thread.Sleep(i); // simulate work item #i 
     token.ThrowIfCancellationRequested(); 
    } 
}), token); 
+0

Wenn also ein Token "Task.Run" statt explizit über einen Parameter implizit übergeben wird, wird es als jede andere Ausnahme innerhalb des Lambda betrachtet. Ich habe versucht, die CancellationToken-Registrierung zu durchsuchen, konnte aber in der Quelle –

+0

@YuvalItzchakov nicht viel herausfinden, das gilt für 'Task.Run'. Für eine einfache 'async Task'-Methode wird die Magie jedoch durch den' async/await'-Compiler-Infrastrukturcode ausgeführt: 'async Task TestAsync (CancellationToken-Token) {token.ThrowIfCancellationRequested(); } 'würde eine' Task' mit 'task.IsCanceled == true' zurückgeben, wenn eine Stornierung auf dem Token angefordert wurde. – Noseratio

+0

Das ist interessant. Ich frage mich, warum sie sich anders verhalten. Würde sich ein asynchrones Lambda wie "task1" oder "task2" verhalten? oder wird es je nach Art des Rückgabeteilnehmers unterschiedlich sein? –

Verwandte Themen