2012-12-10 10 views
27

Mam słabe pojęcie o tym, jak działa słowo kluczowe await i chcę nieco rozszerzyć moje zrozumienie.Rekurencja i oczekiwanie/asynchronizacja Słowa kluczowe

Kwestią, która wciąż sprawia, że ​​moje obroty głową są użycie rekursji. Oto przykład:

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

Ten oczywiście rzuca StackOverflowException.

Moje zrozumienie jest takie, że kod faktycznie działa synchronicznie do pierwszego działania asynchronicznego, po czym zwraca obiekt Task, który zawiera informacje o operacji asynchronicznej. W tym przypadku nie ma operacji asynchronicznej, a więc po prostu powtarza się pod fałszywą obietnicą, że w końcu otrzyma zwrócony kod w postaci Task.

Teraz zmieniając go tylko odrobinę:

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

Ten jeden nie rzucać StackOverflowException. Mogę sortof zobaczyć, dlaczego to działa, ale nazwałbym to bardziej uczuciem jelitowym (prawdopodobnie zajmuje się tym, jak kod jest zorganizowany do korzystania z wywołań zwrotnych, aby uniknąć budowania stosu, ale nie mogę przetłumaczyć tego przeczucia na wyjaśnienie)

mam więc dwa pytania:

  • jaki sposób druga partia kodu uniknąć się StackOverflowException?
  • Czy druga partia kodu marnuje inne zasoby? (na przykład czy przydziela się absurdalnie dużą liczbę obiektów zadań na stercie?)

Dzięki!

Odpowiedz

16

Część do pierwszego czeka w dowolnej funkcji działa synchronicznie. W pierwszym przypadku uruchamia się z powodu przepełnienia stosu - nic nie zakłóca wywoływania funkcji.

Pierwsze oczekiwanie (które nie kończy się natychmiastowo - dotyczy to ciebie z dużym prawdopodobieństwem) powoduje powrót funkcji (i rezygnację z miejsca na stosie!). Kolejka to kolejka jako kontynuacja. TPL zapewnia, że ​​kontynuacja nigdy nie zagnieździ się zbyt głęboko. Jeśli istnieje ryzyko przepełnienia stosu, kontynuacja jest umieszczana w kolejce do puli wątków, resetując stos (który zaczynał się zapełniać).

Drugi przykład może nadal przepełniać! Co zrobić, jeśli zadanie Task.Run zawsze jest wykonywane natychmiast? (Jest to mało prawdopodobne, ale możliwe z odpowiednim harmonogramem wątków systemu operacyjnego). Wtedy funkcja asynchroniczna nigdy nie zostanie przerwana (powodując, że powróci i zwolni całą przestrzeń stosu) i zachowa się tak samo, jak w przypadku 1.

+0

Więc jeśli zastąpiłem 'Task.Run()' funkcją, która natychmiastowo zwróciła ukończony obiekt 'Task', to ponownie wprowadził wyjątek przepełnienia stosu? (A może po prostu zaproponowałem coś, co nie jest możliwe?) – riwalk

+0

@ Stargazer712 Nie tylko jest to możliwe, ale rzeczywiście zrobiłoby to, co opisujesz. Po prostu spróbuj 'poczekaj na Task.FromResult (null);' Oto dlaczego powinieneś unikać jednej funkcji, która "dzwoni" dużo w krótkim czasie; powinieneś upewnić się, że oczekiwane zadania są albo dość długie w działaniu, albo że istnieje ich mała skończona liczba tak, że ich synchroniczne uruchamianie jest w porządku. – Servy

+5

@ Stargazer712 yes! A jeśli zastąpisz go 'Task.Yield()' (co gwarantuje, że będziesz musiał opublikować kontynuację), uzyskasz gwarancję, że nie będziesz mógł przepełnić stosu (kosztem wydajności). Zauważ, że 'Wydajność' zwraca wartość inną niż" Zadanie ". O wiele trudniej jest uzyskać tę gwarancję z zadania, ponieważ musiałbyś dopilnować, aby nie została ona ukończona po zapytaniu o funkcję oczekującą. – usr

0

W pierwszym i drugim przykładzie TestAsync wciąż czeka na powrót do siebie. Różnica polega na tym, że rekurencja polega na drukowaniu i przywracaniu wątku do innej pracy w drugiej metodzie. Dlatego rekursja nie jest wystarczająco szybka, aby mogła być przepełnieniem stosu. Jednak pierwsze zadanie wciąż czeka i ostatecznie liczba osiągnie maksymalną liczbę całkowitą lub przepełnienie stosu zostanie ponownie wyrzucone. Chodzi o to, że zwracany jest wątek, ale właściwa metoda asynchroniczna jest zaplanowana dla tego samego wątku. Zasadniczo, metoda TestAsync została zapomniana, dopóki oczekiwanie nie zostanie zakończone, ale nadal będzie przechowywana w pamięci.Wątek może robić inne rzeczy, dopóki nie zostanie ukończony, a następnie wątek zostanie zapamiętany i zakończy się tam, gdzie czeka się go od lewej. Dodatkowe oczekujące połączenia przechowują wątek i zapominają go ponownie, dopóki oczekiwanie się nie zakończy. Dopóki wszystkie oczekiwania nie zostaną zakończone, a metoda zakończy się, TaskAsync jest nadal w pamięci. A więc o to chodzi. Jeśli powiem metodę, aby coś zrobić, a następnie zadzwoń, poczekaj na zadanie. Reszta moich kodów w innym miejscu nadal działa. Kiedy oczekiwanie jest zakończone, kod podnosi tam i kończy, a następnie wraca do tego, co robił w tym czasie tuż przed nim. W twoich przykładach twoja TaskAsync jest zawsze w stanie nagłośnienia (że tak powiem) aż do zakończenia ostatniego połączenia i zwraca połączenia z powrotem do łańcucha.

EDYCJA: Ciągle mówiłem przechowywać wątek lub wątek i miałem na myśli rutynę. Wszystkie są w tym samym wątku, który jest głównym wątkiem w twoim przykładzie. Przepraszam, jeśli cię pomyliłem.

+1

Ale nie stosujemy tutaj stosu, tylko łańcuch sterty przydzielonych zadań, które wskazują na siebie nawzajem i czeka na zawsze. –

Powiązane problemy