Co dokładnie dzieje się, gdy wątek oczekuje na zadanie w pętli while?

10

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, Taskktó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.

aoven
źródło
1
Zobacz także W jaki sposób wydajność i oczekiwanie na wdrożenie przepływu kontroli w .NET? po świetne informacje o tym, jak to wszystko jest połączone.
John Wu
@John Wu: Nie widziałem jeszcze tego wątku SO. Jest tam wiele interesujących samorodków informacyjnych. Dzięki!
aoven

Odpowiedzi:

6

Możesz to sprawdzić w Try Roslyn . Twoja metoda oczekiwania zostanie przepisana w void IAsyncStateMachine.MoveNext()wygenerowanej klasie asynchronicznej.

Zobaczysz coś takiego:

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

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/awaittylko nad jednym wątkiem.

Mason Wheeler
źródło
2
Wydaje mi się, że nie doceniam zakresu przepisywania tego kompilatora na moim kodzie asynchronicznym. Zasadniczo po przepisaniu nie ma „pętli”! To była dla mnie brakująca część. Niesamowite i dziękuję również za link „Spróbuj Roslyn”!
aoven
GOTO jest oryginalną konstrukcją pętli. Nie zapominajmy o tym.
2

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 kluczowych asynci await; 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()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

To zajmuje trochę czasu, ale podsumowując:

  • continuationreprezentuje zamknięcie „bieżącej iteracji”
  • previousreprezentuje stan Taskzawierający „poprzednią iterację” (tzn. wie, kiedy „iteracja” jest zakończona i służy do rozpoczęcia następnej…)
  • Zakładając , że GetWorkAsync()zwraca a Task, oznacza to , że ContinueWith(_ => GetWorkAsync())zwróci Task<Task>stąd wezwanie do Unwrap()wykonania „wewnętrznego zadania” (tj. Faktycznego wyniku GetWorkAsync()).

Więc:

  1. Początkowo nie ma poprzedniej iteracji, więc po prostu przypisuje się jej wartość Task.FromResult(_quit) - jej stan zaczyna się od Task.Completed == true.
  2. continuationJest uruchamiany po raz pierwszy za pomocąprevious.ContinueWith(continuation)
  3. te continuationaktualizacje zamykające previousodzwierciedlać stan zaawansowania_ => GetWorkAsync()
  4. Po _ => GetWorkAsync()zakończeniu „kontynuuje”, _previous.ContinueWith(continuation)tzn. continuationPonownie wywołuje lambda
    • Oczywiście w tym momencie previouszostał zaktualizowany o stan, _ => GetWorkAsync()więc continuationlambda jest wywoływana po GetWorkAsync()powrocie.

continuationLambda zawsze sprawdza stan _quittak, jeśli _quit == falsepotem nie ma już kontynuacje, a TaskCompletionSourcezostanie 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 .ConfigureAwaitgdzieś?)

Ben Cottrell
źródło
2
Kod, który pokazałem, jest (bardzo) uproszczony. Wewnątrz GetWorkAsync () czeka wiele dalszych oczekiwań. Niektóre z nich uzyskują dostęp do bazy danych i sieci, co oznacza prawdziwe operacje we / wy. Jak rozumiem, zmiana wątku jest naturalną konsekwencją (choć nie jest to wymagane) takich oczekiwań, ponieważ początkowy wątek nie ustanawia żadnego kontekstu synchronizacji, który decydowałby o tym, gdzie powinny być wykonywane kontynuacje. Więc wykonują na wątku puli wątków. Czy moje rozumowanie jest nieprawidłowe?
aoven
@aoven Dobra uwaga - nie brałem pod uwagę różnych rodzajów 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śli awaitjest 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łanie awaitkontekstu jednowątkowego, takiego jak WPF Dispatcher lub Winforms, powinno wystarczyć, aby zapewnić kontynuację oryginału. wątek
Ben Cottrell,