2012-12-10 10 views
27

Ich habe ein zerbrechliches Verständnis davon, wie das await Schlüsselwort funktioniert, und ich möchte mein Verständnis davon ein wenig erweitern.Rekursion und die Erwartung/async Keywords

Das Problem, das meinen Kopf noch dreht, ist die Verwendung von Rekursion. Hier ein Beispiel:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks; 

namespace TestingAwaitOverflow 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      var task = TestAsync(0); 
      System.Threading.Thread.Sleep(100000); 
     } 

     static async Task TestAsync(int count) 
     { 
      Console.WriteLine(count); 
      await TestAsync(count + 1); 
     } 
    } 
} 

Dieses offensichtlich StackOverflowException wirft.

Mein Verständnis ist, weil der Code tatsächlich synchron läuft bis die erste asynchrone Aktion, nach der es ein Task Objekt zurückgibt, das Informationen über den asynchronen Vorgang enthält. In diesem Fall gibt es keine asynchrone Operation, so dass es nur unter dem falschen Versprechen weiterläuft, dass es schließlich eine Task zurückgegeben bekommt.

Jetzt ist es nur ein kleines bisschen zu ändern:

using System; 
using System.Collections.Generic; 
using System.Linq; 
using System.Text; 
using System.Threading.Tasks; 

namespace TestingAwaitOverflow 
{ 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      var task = TestAsync(0); 
      System.Threading.Thread.Sleep(100000); 
     } 

     static async Task TestAsync(int count) 
     { 
      await Task.Run(() => Console.WriteLine(count)); 
      await TestAsync(count + 1); 
     } 
    } 
} 

Dieses kein StackOverflowException wirft. Ich kann sortof sehen, warum es funktioniert, aber ich würde es eher ein Bauchgefühl nennen (es geht wahrscheinlich damit um, wie der Code Callbacks verwenden soll, um den Stack zu vermeiden, aber ich kann dieses Bauchgefühl nicht in einen übersetzen Erklärung)

So habe ich zwei Fragen:

  • Wie funktioniert die zweite Charge von Code ein StackOverflowException vermeiden?
  • Vergeudet der zweite Stapel von Code andere Ressourcen? (Zum Beispiel weist es eine absurd große Anzahl von Aufgabenobjekten auf dem Haufen zu?)

Danke!

Antwort

16

Der Teil bis zum ersten erwarten in jeder Funktion läuft synchron. Im ersten Fall kommt es deshalb zu einem Stack-Overflow - die Funktion selbst wird durch nichts unterbrochen.

Die ersten erwarten (die nicht sofort abgeschlossen werden - das ist bei Ihnen mit hoher Wahrscheinlichkeit der Fall) bewirkt, dass die Funktion zurückkehrt (und ihren Stapelspeicherplatz aufgibt!). Es stellt den Rest als Fortsetzung dar. Die TPL sorgt dafür, dass Fortsetzungen niemals zu tief nisten. Wenn das Risiko eines Stapelüberlaufs besteht, wird die Fortsetzung in den Thread-Pool eingereiht, wodurch der Stapel (der sich zu füllen beginnt) zurückgesetzt wird.

Das zweite Beispiel kann immer noch überlaufen! Was ist, wenn die Task.Run Aufgabe immer sofort ausgeführt wird? (Dies ist unwahrscheinlich, aber mit der richtigen OS-Thread-Planung möglich). Dann würde die asynchrone Funktion niemals unterbrochen werden (wodurch sie zurückkehren und den gesamten Stapelspeicherplatz freigeben würde) und das gleiche Verhalten wie in Fall 1 würde sich ergeben.

+0

Also, wenn ich die 'Task.Run()' mit einer Funktion ersetzt, die sofort ein abgeschlossenes 'Task'-Objekt zurückgab, würde es die Stapelüberlauf-Ausnahme wieder einführen? (Oder habe ich gerade etwas vorgeschlagen, was nicht möglich ist?) – riwalk

+0

@ Stargazer712 Nicht nur ist es möglich, es würde tatsächlich tun, was du beschreibst. Probieren Sie 'aware Task.FromResult (null);' Aus diesem Grund sollten Sie vermeiden, dass eine einzelne Funktion in kurzer Zeit 'wartet'. Sie sollten sicherstellen, dass erwartete Aufgaben entweder ziemlich lange in Betrieb sind oder dass es eine kleine endliche Anzahl von ihnen gibt, so dass sie synchron laufen. – Servy

+5

@ Stargazer712 ja! Und wenn Sie es mit 'Task.Yield()' ersetzen (was garantiert, dass Sie eine Fortsetzung buchen müssen), würden Sie garantiert frei von Stack-Überläufen sein (bei einer Performance-Kosten). Beachten Sie, dass "Yield" eine erwartete andere als "Task" zurückgibt. Es ist viel schwieriger, diese Garantie von einer Aufgabe zu erhalten, da Sie sicherstellen müssen, dass sie nicht abgeschlossen wird, wenn Sie von der Funktion "Warten" abgefragt werden. – usr

0

In Ihrem ersten und zweiten Beispiel wartet der TestAsync immer noch auf den Aufruf an sich selbst zurückzukehren. Der Unterschied besteht darin, dass die Rekursion den Thread druckt und an die andere Arbeit der zweiten Methode zurückgibt. Daher ist die Rekursion nicht schnell genug, um ein Stapelüberlauf zu sein. Die erste Aufgabe wartet jedoch immer noch und die Zählung erreicht schließlich ihre maximale Ganzzahlgröße oder der Stapelüberlauf wird erneut ausgelöst. Der Punkt ist, dass der aufrufende Thread zurückgegeben wird, aber die tatsächliche asynchrone Methode für denselben Thread geplant ist. Grundsätzlich ist die TestAsync-Methode vergessen, bis die Wartezeit vollständig ist, aber noch im Speicher gehalten wird.Der Thread darf andere Dinge tun, bis er fertig ist, und dann wird dieser Thread erinnert und beendet, wo warten gelassen wird. Zusätzliche Warteaufrufe speichern den Thread und vergessen ihn wieder, bis er wieder fertig ist. Bis alle Warten abgeschlossen sind und die Methode damit abgeschlossen ist, befindet sich der TaskAsync noch im Speicher. Also, hier ist die Sache. Wenn ich eine Methode erkläre etwas zu tun und dann auf eine Aufgabe warten. Der Rest meiner Codes läuft weiter. Wenn die Wartezeit vollständig ist, nimmt der Code die Daten wieder auf und endet und kehrt dann zu dem zurück, was er gerade davor getan hat. In Ihren Beispielen befindet sich Ihr TaskAsync immer sozusagen in einem verfallenen Zustand, bis der letzte Aufruf abgeschlossen ist und die Aufrufe die Kette zurücksenden.

EDIT: Ich sagte immer speichern den Thread oder den Thread und ich meinte Routine. Sie befinden sich alle auf demselben Thread, der in Ihrem Beispiel der Hauptthread ist. Entschuldigung, wenn ich dich verwirrt habe.

+1

Hier wird jedoch kein Stapel verwendet, sondern nur eine Kette von Heap-Tasks, die auf einander zeigen und ewig warten. –