2016-09-02 21 views
7

Początkowo myślałem, że wszystkie kontynuacje są wykonywane na wątku (biorąc pod uwagę domyślny kontekst synchronizacji). To jednak nie wydaje się być, gdy używam TaskCompletionSource.W takim przypadku TaskCompletionSource.SetResult() uruchamiać kontynuację synchronicznie?

Mój kod wygląda mniej więcej tak:

Task<int> Foo() { 
    _tcs = new TaskCompletionSource<int>(); 
    return _tcs.Task; 
} 

async void Bar() { 
    Console.WriteLine(Thread.Current.ManagedThreadId); 
    Console.WriteLine($"{Thread.Current.ManagedThreadId} - {await Foo()}"); 
} 

Bar jest wywoływana na konkretnym wątku i TaskCompletionSource pozostaje wyłączony przez jakiś czas, co oznacza, zwróconych zadań IsComplete = false. Potem po pewnym czasie ten sam wątek zadzwoni na numer _tcs.SetResult(x), który według mojego zrozumienia powinien uruchomić kontynuację w wątku.

Ale zaobserwowałem w mojej aplikacji, że wątek kontynuujący kontynuację jest w rzeczywistości wciąż tym samym wątkiem, tak jakby kontynuacja została wywołana synchronicznie dokładnie tak, jak nazywa się SetResult.

Próbowałem nawet ustawić punkt przerwania na SetResult i przechodząc nad nim (i mając punkt przerwania w kontynuacji), który z kolei w rzeczywistości dalej wywołuje synchroniczną kontynuację.

Kiedy dokładnie SetResult() natychmiast wywołuje synchronicznie kontynuację?

+0

Pomoże ci, jeśli podasz [mcve] zamiast * pół * tego, więc możemy sami z nim eksperymentować ... –

Odpowiedz

4

SetResultzwykle uruchamia synchronicznie kontynuację z TCS. Głównym wyjątkiem jest to, że jeśli jawnie przekazać w TaskContinuationOptions.RunContinuationsAsynchronously flagi podczas tworzenia TCS (nowy w .NET 4.6). Innym scenariuszem, w którym działa asynchronicznie, jest sytuacja, gdy wydaje się, że bieżący wątek jest skazany na zagładę.

Jest to bardzo ważne, ponieważ jeśli nie jesteś ostrożny, możesz mieć kod wywołujący kontrolę nad wątkiem, który miał wykonywać inną pracę (jak: obsługa gniazda IO).

+0

BTW: Co się dzieje, gdy wywołasz 'SetResult()' w wątku, który nie jest oryginalnym dzwoniącym? Czy kontynuacja nie powinna próbować dostać się do pierwotnego wątku wywołującego z powodu domyślnego 'ConfigureAwait (...)'? – Petrroll

+0

@Przejmij, jeśli nie było 'SyncronisationContext.Current', gdy funkcja została wywołana, nie będzie miała oryginalnego wątku do przejścia. –

+0

A jeśli był jakiś wyraźny SyncContext? – Petrroll

5

Początkowo myślałem, że wszystkie kontynuacje są wykonywane na wątku (biorąc pod uwagę domyślny kontekst synchronizacji). To jednak nie wydaje się być, gdy używam TaskCompletionSource.

W rzeczywistości przy korzystaniu z await większość kontynuacji jest wykonywana synchronicznie.

Odpowiedź Marca jest świetna; Chciałem tylko trochę bardziej szczegółowo ...

TaskCompletionSource<T> domyślnie będzie działać synchronicznie po wywołaniu Set*. Set* zakończy zadanie i wydać kontynuacje w jednym wywołaniu metody. (Oznacza to, że wywoływanie Set* podczas trzymania zamka jest receptą na zakleszczenia.)

Używam dziwnego wyrażenia "wydajemy kontynuacje", ponieważ może ono faktycznie, ale nie musi, wykonywać je; więcej o tym później.

Flaga TaskCreationOptions.RunContinuationsAsynchronously powie TaskCompletionSource<T>, aby kontynuować asynchronicznie. Powoduje to przerwanie wykonywania zadania (które jest nadal wykonywane natychmiast przez Set*) od wydania kontynuacji (które jest wywoływane tylko przez wywołanie Set*).Tak więc z RunContinuationsAsynchronously, wywołanie Set* zakończy tylko to zadanie; nie będzie wykonywał kontynuacji synchronicznie. (Oznacza to, że dzwonienie pod numer Set* przy użyciu blokady jest bezpieczne.)

Powrót do domyślnego przypadku, który powoduje synchroniczne kontynuacje.

Każda kontynuacja również ma flagę; domyślnie kontynuacja jest wykonywana asynchronicznie, ale może być synchronizowana przez TaskContinuationOptions.ExecuteSynchronously. (Zauważ, że awaitdoes use this flag - link do mojego bloga, technicznie jest to szczegół implementacji i nie jest oficjalnie udokumentowany).

Jednak nawet jeśli ExecuteSynchronously jest określona, ​​istnieje a number of situations where the continuation is not executed synchronously:

  • Jeśli istnieje TaskScheduler związane z kontynuacją, że harmonogram ma możliwość odrzucenia bieżącego wątku, w przypadku których zadaniem jest w kolejce do tego TaskScheduler zamiast wykonywania synchronicznie.
  • Jeśli bieżący wątek jest przerywany, zadanie jest kolejkowane w innym miejscu.
  • Jeśli stos bieżącego wątku jest zbyt głęboki, zadanie jest umieszczane w kolejce w innym miejscu. (Jest to tylko heurystyka i nie gwarantuje się uniknięcia StackOverflowException).

To sporo warunków, ale z prostego testu app konsoli, wszystkie są spełnione:

  • TaskCompletionSource<T> nie precyzuje RunContinuationsAsynchronously.
  • Kontynuacja (await) nie określa ExecuteSynchronously.
  • Kontynuacja nie ma określonego TaskScheduler.
  • Wątek docelowy jest w stanie wykonać kontynuację (nie jest przerywana, stos jest w porządku).

Zgodnie z ogólną zasadą chciałbym powiedzieć, że każde użycie TaskCompletionSource<T> powinno określać TaskCreationOptions.RunContinuationsAsynchronously. Osobiście uważam, że semantyka jest bardziej odpowiednia i mniej zaskakująca w przypadku tej flagi.