2015-07-22 11 views
8

Ich baute einige async/erwarten Demo-Konsole App und seltsames Ergebnis bekommen. Code:C# async/erwarten seltsames Verhalten in der Konsole App

class Program 
{ 
    public static void BeginLongIO(Action act) 
    { 
     Console.WriteLine("In BeginLongIO start... {0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     Thread.Sleep(1000); 
     act(); 
     Console.WriteLine("In BeginLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
    } 

    public static Int32 EndLongIO() 
    { 
     Console.WriteLine("In EndLongIO start... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     Thread.Sleep(500); 
     Console.WriteLine("In EndLongIO end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     return 42; 
    } 

    public static Task<Int32> LongIOAsync() 
    { 
     Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     var tcs = new TaskCompletionSource<Int32>(); 
     BeginLongIO(() => 
     { 
      try { tcs.TrySetResult(EndLongIO()); } 
      catch (Exception exc) { tcs.TrySetException(exc); } 
     }); 
     Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     return tcs.Task; 
    } 

    public async static Task<Int32> DoAsync() 
    { 
     Console.WriteLine("In DoAsync start... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     var res = await LongIOAsync(); 
     Thread.Sleep(1000); 
     Console.WriteLine("In DoAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     return res; 
    } 

    static void Main(String[] args) 
    { 
     ticks = DateTime.Now.Ticks; 
     Console.WriteLine("In Main start... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     DoAsync(); 
     Console.WriteLine("In Main exec... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     Thread.Sleep(3000); 
     Console.WriteLine("In Main end... \t\t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
    } 

    private static Int64 ticks; 
} 

Das Ergebnis Gebrüll:

enter image description here

Vielleicht erwarte ich nicht vollständig verstehen, was genau macht. Ich dachte, wenn die Ausführung wartet, dann kehrt die Ausführung zu der Aufrufermethode und Aufgabe zum Warten auf Läufe in einem anderen Thread zurück. In meinem Beispiel werden alle Operationen in einem Thread ausgeführt und die Ausführung kehrt nach dem await-Schlüsselwort nicht zur Aufrufermethode zurück. Wo ist die Wahrheit?

+0

können Sie sehen folgendermaßen aus: https: //channel9.msdn.com/Shows/Going+Deep/Mads-Torgersen-Inside-C-Async – Chaka

+1

'Ich dachte, wenn die Ausführung dann die Ausführung kehrt zu erwarten kommt an die Aufrufer-Methode und Task für wartende Läufe in einem anderen Thread. Jedes einzelne 'async' Intro, das mir bekannt ist (einschließlich [meiner] (http://blog.stephencleary.com/2012/02/async-and-await .html)) geht aus dem Weg, dies explizit zu sagen * ist nicht * was passiert. –

Antwort

2

So funktioniert async-await nicht.

Das Markieren einer Methode wie async erstellt keine Hintergrundthreads. Wenn Sie eine async Methode aufrufen, läuft es synchron bis zu einem asynchronen Punkt und kehrt dann nur zum Aufrufer zurück.

Dieser asynchrone Punkt ist, wenn Sie eine Aufgabe await noch nicht abgeschlossen haben. Wenn dies abgeschlossen ist, wird der Rest der Methode ausgeführt. Diese Task sollte eine tatsächliche asynchrone Operation darstellen (wie I/O oder Task.Delay).

In Ihrem Code gibt es keinen asynchronen Punkt, es gibt keinen Punkt, in dem der aufrufende Thread zurückgegeben wird. Der Thread geht einfach immer tiefer und blockiert auf Thread.Sleep, bis diese Methoden abgeschlossen sind und DoAsync zurückgibt.

Nehmen Sie dieses einfache Beispiel:

public static void Main() 
{ 
    MainAsync().Wait(); 
} 

public async Task MainAsync() 
{ 
    // calling thread 
    await Task.Delay(1000); 
    // different ThreadPool thread 
} 

Hier haben wir einen tatsächlichen asynchronen Punkt (Task.Delay) die aufrufende Thread kehrt zu Main und dann auf die Aufgabe synchron Blöcke. Nach einer Sekunde ist die Task.Delay Aufgabe abgeschlossen und der Rest der Methode wird auf einem anderen ThreadPool Thread ausgeführt.

Wenn wir anstelle von Task.DelayThread.Sleep verwendet hätten, würde alles auf demselben Aufruf-Thread laufen.

+0

Danke für die schnelle Antwort! Es ist hilfreich. Dann meine nächste Frage: wie warten auf die Aufgabe warten, die von der aufgabenbasierten Wrapper für Methoden, die auf die APM-Muster wie BeginReceive und EndReceive von Socket-Klasse folgen? Dieser Wrapper verwendet TaskCompletionSource und gibt auch den abgeschlossenen Task zurück. Wo gibt es einen asynchronen Punkt? – Jeksonic

+0

@Jeksonic Die TCS-Verwendung ist in Ordnung. Die Sache ist, dass die Begin/End-Methoden in .Net bereits asynchron sind. Wenn Sie 'Begin' aufrufen, startet es einfach eine Operation und gibt einen Thread frei. Nur wenn die Operation abgeschlossen ist, ruft sie den bereitgestellten Rückruf auf. – i3arnon

+0

@Jeksonic In Ihrem Fall. Die 'Begin'-Methode blockiert nur den aufrufenden Thread und ruft dann den Callback auf, so dass der Thread nicht freigegeben wird. – i3arnon

0

die Linie, die tatsächlich etwas auf einem Hintergrund-Thread läuft, ist

Task.Run(() => { }); 

In Ihrem Beispiel Sie erwarten nicht, dass Aufgabe, sondern die eines TaskCompletionSource

public static Task<int> LongIOAsync() 
    { 
     var tcs = new TaskCompletionSource<Int32>(); 

     Task.Run (() => BeginLongIO(() => 
     { 
      try { tcs.TrySetResult(EndLongIO()); } 
      catch (Exception exc) { tcs.TrySetException(exc); } 
     })); 

     return tcs.Task;  
    } 

Wenn LongIOAsync wartet auf Sie warten auf die Die Aufgabe von tcs witch wird aus einem Hintergrundthread in einem Delegat für Task.Run()

festgelegt. Wenden Sie diese Änderung an:

public static Task<Int32> LongIOAsync() 
    { 
     Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     var tcs = new TaskCompletionSource<Int32>(); 

     Task.Run (() => BeginLongIO(() => 
     { 
      try { tcs.TrySetResult(EndLongIO()); } 
      catch (Exception exc) { tcs.TrySetException(exc); } 
     })); 

     Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
     return tcs.Task; 
    } 

Abwechselnd in diesem Fall, dass Sie nur erwartete die von Task.Run() zurückgegeben von könnten, TaskCompletionSource ist für Situationen, in denen Sie um die Fähigkeit zu übergeben wollen, um Ihre Aufgabe als Allein- oder andere weise zu setzen.

0

Kurze Antwort ist, dass LongIOAsync() blockiert.Wenn Sie dies in einem GUI-Programm ausführen, werden Sie sehen, dass die GUI kurzzeitig einfriert - nicht so, wie async/await funktionieren soll. Daher fällt das Ganze auseinander.

Sie müssen alle lang laufenden Operationen in eine Aufgabe umbrechen dann direkt auf diese Aufgabe warten. Nichts sollte dabei blockieren.

1

Um dieses Verhalten wirklich zu verstehen, müssen Sie zuerst verstehen, was Task ist und was async und await tatsächlich tun, um Ihren Code.

Task ist die CLR-Darstellung von "einer Aktivität". Es könnte eine Methode sein, die auf einem Worker-Pool-Thread ausgeführt wird. Es könnte eine Operation sein, um Daten aus einer Datenbank über ein Netzwerk abzurufen. Seine generische Natur erlaubt es, viele verschiedene Implementierungen zu kapseln, aber grundsätzlich müssen Sie verstehen, dass es nur "eine Aktivität" bedeutet.

Die Task Klasse gibt Ihnen Möglichkeiten, den Status der Aktivität zu überprüfen: ob es abgeschlossen ist, ob es noch zu starten hat, ob es einen Fehler generiert hat usw. Diese Modellierung einer Aktivität ermöglicht es uns, Programme leichter zu erstellen die als Folge von Aktivitäten erstellt werden und nicht als Folge von Methodenaufrufen.

diesen trivialen Code Bedenken Sie:

public void FooBar() 
{ 
    Foo(); 
    Bar(); 
} 

Was dies bedeutet, ist „Execute-Methode Foo, dann ausführen Methode Bar Wenn wir eine Implementierung berücksichtigen, die Task von Foo und Bar, die Zusammensetzung dieser Anrufe zurückgibt, ist anders. :

public void FooBar() 
{ 
    Foo().Wait(); 
    Bar().Wait(); 
} 

die Bedeutung „Start ist nun eine Aufgabe für sie zu finis die Methode Foo und warten mit h, dann starte einen Task mit der Methode Bar und warte, bis er beendet ist. "Der Aufruf Wait() auf einem Task ist selten richtig - es bewirkt, dass der aktuelle Thread blockiert, bis der Task abgeschlossen ist und unter einigen häufig verwendeten Threading-Modellen Deadlocks verursachen kann. Stattdessen können wir async und await verwenden, um ohne diesen gefährlichen Aufruf einen ähnlichen Effekt zu erzielen.

public async Task FooBar() 
{ 
    await Foo(); 
    await Bar(); 
} 

Das async Schlüsselwort bewirkt die Ausführung Ihrer Methode in Stücke zerlegt wird: jedes Mal, wenn Sie await schreiben, es den folgenden Code nimmt und eine „Fortsetzung“: ein Verfahren, als nach dem Task ausgeführt werden erwartete Aufgabe wird abgeschlossen.

Dies unterscheidet sich von Wait(), da Task nicht mit einem bestimmten Ausführungsmodell verknüpft ist. Wenn die Task zurückgegeben von Foo() einen Anruf über das Netzwerk darstellt, gibt es keine Thread blockiert, warten auf das Ergebnis - es gibt eine Task wartet auf den Vorgang abgeschlossen. Wenn der Vorgang abgeschlossen ist, wird die Ausführung der Task geplant - dieser Planungsprozess ermöglicht eine Trennung zwischen der Definition der Aktivität und der Methode, mit der sie ausgeführt wird, und sie ist die Stärke bei der Verwendung von Aufgaben.

das Verfahren kann also zusammengefasst werden als:

    , die Aufgabe starten
  • Foo()
  • , wenn diese Aufgabe Bar
  • wenn diese Aufgabe abgeschlossen ist die Methode Aufgabe die Aufgabe starten anzuzeigen,
  • abgeschlossen

In Ihrer Konsole App warten Sie nicht auf y Task, die die ausstehende E/A-Operation darstellt, weshalb Sie einen blockierten Thread sehen - es gibt nie eine Möglichkeit, eine Fortsetzung einzurichten, die asynchron ausgeführt wird.

Wir können Ihre LongIOAsync-Methode reparieren, um Ihre Long IO asynchron mit der Methode Task.Delay() zu simulieren. Diese Methode gibt eine Task zurück, die nach einer bestimmten Zeitspanne abgeschlossen wird. Dies gibt uns die Möglichkeit für eine asynchrone Fortsetzung.

public static async Task<Int32> LongIOAsync() 
{ 
    Console.WriteLine("In LongIOAsync start... {0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 

    await Task.Delay(1000);  

    Console.WriteLine("In LongIOAsync end... \t{0} {1}", (DateTime.Now.Ticks - ticks)/TimeSpan.TicksPerMillisecond, Thread.CurrentThread.ManagedThreadId); 
}