Po pewnym czasie zajmowania się wzorcem asynchronizacji / oczekiwania w języku C # nagle zdałem sobie sprawę, że tak naprawdę nie wiem, jak wyjaśnić, co dzieje się w następującym kodzie:
async void MyThread()
{
while (!_quit)
{
await GetWorkAsync();
}
}
GetWorkAsync()
zakłada się, że zwróci oczekiwany element, Task
który może, ale nie musi, spowodować przełączenie wątku podczas kontynuacji.
Nie byłbym zdezorientowany, gdyby oczekiwanie nie było w pętli. Naturalnie oczekiwałbym, że reszta metody (tj. Kontynuacja) potencjalnie zadziała w innym wątku, co jest w porządku.
Jednak wewnątrz pętli koncepcja „reszty metody” staje się dla mnie nieco mglista.
Co stanie się z „resztą pętli”, jeśli wątek zostanie włączony w kontynuacji, a jeśli nie zostanie przełączony? W którym wątku wykonywana jest następna iteracja pętli?
Moje obserwacje pokazują (niepotwierdzone jednoznacznie), że każda iteracja rozpoczyna się w tym samym wątku (oryginalnym), podczas gdy kontynuacja jest wykonywana w innym. Czy to może być naprawdę? Jeśli tak, to czy jest to stopień nieoczekiwanej równoległości, który należy uwzględnić w odniesieniu do bezpieczeństwa wątków metody GetWorkAsync?
AKTUALIZACJA: Moje pytanie nie jest duplikatem, jak niektórzy sugerują. while (!_quit) { ... }
Wzór kod jest jedynie uproszczenie mojego aktualnego kodu. W rzeczywistości mój wątek jest długotrwałą pętlą, która przetwarza swoją kolejkę wprowadzania elementów pracy w regularnych odstępach czasu (domyślnie co 5 sekund). Rzeczywista kontrola warunków wyjścia nie jest również prostą kontrolą pola, jak sugeruje przykładowy kod, ale raczej kontrolą uchwytu zdarzenia.
Odpowiedzi:
Możesz to sprawdzić w Try Roslyn . Twoja metoda oczekiwania zostanie przepisana w
void IAsyncStateMachine.MoveNext()
wygenerowanej klasie asynchronicznej.Zobaczysz coś takiego:
Zasadniczo nie ma znaczenia, w którym wątku się znajdujesz; automat stanowy może wznowić poprawnie, zastępując pętlę równoważną strukturą if / goto.
Powiedziawszy to, metody asynchroniczne niekoniecznie działają na innym wątku. Zobacz wyjaśnienie Erica Lipperta „To nie jest magia”, aby wyjaśnić, w jaki sposób możesz pracować
async/await
tylko nad jednym wątkiem.źródło
Po pierwsze, Servy napisała kod w odpowiedzi na podobne pytanie, na której opiera się ta odpowiedź:
/programming/22049339/how-to-create-a-cancellable-task-loop
Odpowiedź Servy obejmuje podobną
ContinueWith()
pętlę z wykorzystaniem konstruktów TPL bez wyraźnego użycia słów kluczowychasync
iawait
; więc aby odpowiedzieć na pytanie, zastanów się, jak może wyglądać twój kod, gdy twoja pętla jest rozwijana za pomocąContinueWith()
To zajmuje trochę czasu, ale podsumowując:
continuation
reprezentuje zamknięcie „bieżącej iteracji”previous
reprezentuje stanTask
zawierający „poprzednią iterację” (tzn. wie, kiedy „iteracja” jest zakończona i służy do rozpoczęcia następnej…)GetWorkAsync()
zwraca aTask
, oznacza to , żeContinueWith(_ => GetWorkAsync())
zwróciTask<Task>
stąd wezwanie doUnwrap()
wykonania „wewnętrznego zadania” (tj. Faktycznego wynikuGetWorkAsync()
).Więc:
Task.FromResult(_quit)
- jej stan zaczyna się odTask.Completed == true
.continuation
Jest uruchamiany po raz pierwszy za pomocąprevious.ContinueWith(continuation)
continuation
aktualizacje zamykająceprevious
odzwierciedlać stan zaawansowania_ => GetWorkAsync()
_ => GetWorkAsync()
zakończeniu „kontynuuje”,_previous.ContinueWith(continuation)
tzn.continuation
Ponownie wywołuje lambdaprevious
został zaktualizowany o stan,_ => GetWorkAsync()
więccontinuation
lambda jest wywoływana poGetWorkAsync()
powrocie.continuation
Lambda zawsze sprawdza stan_quit
tak, jeśli_quit == false
potem nie ma już kontynuacje, aTaskCompletionSource
zostanie ustawiona na wartości_quit
, a wszystko zostanie zakończone.Jeśli chodzi o twoją obserwację dotyczącą kontynuacji wykonywanej w innym wątku, nie jest to coś, co zrobiłoby dla ciebie słowo kluczowe
async
/await
, jak na tym blogu „Zadania nie są (wciąż) wątkami, a asynchronizacja nie jest równoległa” . - https://blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/Sugeruję, że naprawdę warto przyjrzeć się twojej
GetWorkAsync()
metodzie w odniesieniu do gwintowania i bezpieczeństwa nici. Jeśli diagnostyka ujawnia, że jest wykonywana w innym wątku w wyniku powtarzającego się kodu asynchronicznego / oczekującego, to coś w tej metodzie lub związane z tą metodą musi powodować utworzenie nowego wątku w innym miejscu. (Jeśli jest to nieoczekiwane, być może.ConfigureAwait
gdzieś?)źródło
SynchronizationContext
- jest to z pewnością ważne, ponieważ.ContinueWith()
używa SynchronizationContext do wysłania kontynuacji; to rzeczywiście wyjaśnia zachowanie, które widzisz, jeśliawait
jest wywoływane w wątku ThreadPool lub wątku ASP.NET. W takich przypadkach kontynuacja może być z pewnością wysłana do innego wątku. Z drugiej strony wywołanieawait
kontekstu jednowątkowego, takiego jak WPF Dispatcher lub Winforms, powinno wystarczyć, aby zapewnić kontynuację oryginału. wątek