Kiedy powinienem użyć Task.Yield ()?

218

Używam async / czekaj i Taskdużo, ale nigdy nie korzystałem Task.Yield()i szczerze mówiąc, pomimo wszystkich wyjaśnień, nie rozumiem, dlaczego potrzebuję tej metody.

Czy ktoś może podać dobry przykład, gdzie Yield()jest to wymagane?

Krumelur
źródło

Odpowiedzi:

241

Kiedy używasz async/ await, nie ma gwarancji, że metoda, którą wywołasz, kiedy to zrobisz, await FooAsync()będzie działać asynchronicznie. Wewnętrzna implementacja może powrócić za pomocą całkowicie synchronicznej ścieżki.

Jeśli tworzysz interfejs API, w którym bardzo ważne jest, aby nie blokować i uruchamiać jakiś kod asynchronicznie, a istnieje szansa, że ​​wywoływana metoda będzie działać synchronicznie (efektywnie blokując), użycie await Task.Yield()spowoduje wymuszenie asynchroniczności metody i zwróci kontrola w tym momencie. Reszta kodu zostanie wykonana w późniejszym czasie (w tym momencie nadal może działać synchronicznie) w bieżącym kontekście.

Może to być również przydatne, jeśli wykonasz metodę asynchroniczną, która wymaga pewnej „długofalowej” inicjalizacji, tj .:

 private async void button_Click(object sender, EventArgs e)
 {
      await Task.Yield(); // Make us async right away

      var data = ExecuteFooOnUIThread(); // This will run on the UI thread at some point later

      await UseDataAsync(data);
 }

Bez Task.Yield()wywołania metoda będzie wykonywana synchronicznie aż do pierwszego wywołania await.

Reed Copsey
źródło
26
Wydaje mi się, że coś tutaj źle interpretuję. Jeśli await Task.Yield()zmusza metodę do asynchronizacji, dlaczego mielibyśmy zawracać sobie głowę pisaniem „prawdziwego” kodu asynchronicznego? Wyobraź sobie metodę intensywnej synchronizacji. Aby było asynchroniczne, wystarczy dodać, asynca await Task.Yield()na początku i magicznie będzie asynchroniczne? To byłoby prawie jak zawijanie całego kodu synchronizacji Task.Run()i tworzenie fałszywej metody asynchronicznej.
Krumelur
14
@Krumelur Jest duża różnica - spójrz na mój przykład. Jeśli używasz Task.Runzaimplementować go, ExecuteFooOnUIThreadbędzie działać na puli wątków, a nie na wątku interfejsu użytkownika. Za pomocą await Task.Yield()wymusza się jego asynchroniczność w taki sposób, że kolejny kod jest nadal uruchamiany w bieżącym kontekście (tylko w późniejszym momencie). To nie jest coś, co zwykle robisz, ale fajnie, że istnieje opcja, jeśli jest wymagana z jakiegoś dziwnego powodu.
Reed Copsey
7
Jeszcze jedno pytanie: gdyby ExecuteFooOnUIThread()działało bardzo długo, w pewnym momencie nadal blokowałoby wątek interfejsu użytkownika i powodowało brak reakcji interfejsu, czy to prawda?
Krumelur
7
@Krumelur Tak, tak. Po prostu nie od razu - stanie się to później.
Reed Copsey
33
Chociaż odpowiedź ta jest technicznie poprawna, stwierdzenie, że „reszta kodu zostanie wykonana później” jest zbyt abstrakcyjne i może wprowadzać w błąd. Harmonogram wykonania kodu po Task.Yield () jest bardzo zależny od konkretnego kontekstu synchronizacji. A dokumentacja MSDN wyraźnie stwierdza, że ​​„Kontekst synchronizacji obecny w wątku interfejsu użytkownika w większości środowisk interfejsu użytkownika często priorytetowo traktuje pracę wysłaną do kontekstu wyższą niż praca wprowadzania i renderowania. Z tego powodu nie należy polegać na oczekiwaniu na Task.Yield () ; aby interfejs był responsywny. ”
Vitaliy Tsvayer,
36

Wewnętrznie await Task.Yield()po prostu kolejkuje kontynuację w bieżącym kontekście synchronizacji lub w wątku losowej puli, jeśli SynchronizationContext.Currentjest null.

Jest efektywnie wdrażany jako niestandardowe oczekiwanie. Mniej wydajny kod dający identyczny efekt może być tak prosty:

var tcs = new TaskCompletionSource<bool>();
var sc = SynchronizationContext.Current;
if (sc != null)
    sc.Post(_ => tcs.SetResult(true), null);
else
    ThreadPool.QueueUserWorkItem(_ => tcs.SetResult(true));
await tcs.Task;

Task.Yield()może być użyty jako skrót do niektórych dziwnych zmian przepływu wykonania. Na przykład:

async Task DoDialogAsync()
{
    var dialog = new Form();

    Func<Task> showAsync = async () => 
    {
        await Task.Yield();
        dialog.ShowDialog();
    }

    var dialogTask = showAsync();
    await Task.Yield();

    // now we're on the dialog's nested message loop started by dialog.ShowDialog 
    MessageBox.Show("The dialog is visible, click OK to close");
    dialog.Close();

    await dialogTask;
    // we're back to the main message loop  
}

To powiedziawszy, nie mogę wymyślić żadnego przypadku, w którym Task.Yield()nie można go zastąpić Task.Factory.StartNeww / właściwym harmonogramem zadań.

Zobacz też:

noseratio
źródło
W twoim przykładzie, jaka jest różnica między tym, co tam jest a var dialogTask = await showAsync();?
Erik Philips
@ErikPhilips, var dialogTask = await showAsync()nie skompiluje się, ponieważ await showAsync()wyrażenie nie zwraca a Task(inaczej niż bez await). To powiedziawszy, jeśli to zrobisz await showAsync(), wykonanie po nim zostanie wznowione dopiero po zamknięciu okna dialogowego, właśnie tak jest inaczej. To dlatego, że window.ShowDialogjest synchronicznym interfejsem API (mimo że nadal pompuje wiadomości). W tym kodzie chciałem kontynuować, dopóki okno dialogowe jest nadal wyświetlane.
noseratio
5

Jednym z zastosowań Task.Yield()jest zapobieganie przepełnieniu stosu podczas wykonywania rekurencji asynchronicznej. Task.Yield()zapobiega synchronicznej kontynuacji. Należy jednak pamiętać, że może to spowodować wyjątek OutOfMemory (jak zauważył Triynko). Niekończąca się rekurencja wciąż nie jest bezpieczna i prawdopodobnie lepiej jest przepisać rekurencję jako pętlę.

private static void Main()
    {
        RecursiveMethod().Wait();
    }

    private static async Task RecursiveMethod()
    {
        await Task.Delay(1);
        //await Task.Yield(); // Uncomment this line to prevent stackoverlfow.
        await RecursiveMethod();
    }
Joakim MH
źródło
4
Może to zapobiec przepełnieniu stosu, ale w końcu zabraknie mu pamięci systemowej, jeśli pozwolisz mu działać wystarczająco długo. Każda iteracja tworzy nowe Zadanie, które nigdy się nie kończy, ponieważ Zadanie zewnętrzne oczekuje na Zadanie wewnętrzne, które czeka na kolejne Zadanie wewnętrzne i tak dalej. To nie jest OK. Alternatywnie, możesz po prostu mieć jedno zadanie zewnętrzne, które nigdy się nie kończy, i po prostu mieć pętlę zamiast powtarzania. Zadanie nigdy się nie zakończy, ale będzie tylko jeden z nich. Wewnątrz pętli może przynieść lub poczekać na wszystko, co chcesz.
Triynko
Nie mogę odtworzyć przepełnienia stosu. Wydaje się, że await Task.Delay(1)wystarczy, aby temu zapobiec. (Aplikacja konsoli, .NET Core 3.1, C # 8)
Theodor Zoulias
-8

Task.Yield() może być stosowany w próbnych implementacjach metod asynchronicznych.

mhsirig
źródło
4
Powinieneś podać kilka szczegółów.
PJProudhon
3
W tym celu wolę użyć Task.CompletedTask - więcej informacji znajduje się w sekcji Task.CompletedTask w tym blogu msdn .
Grzegorz Smulko
2
Problem z użyciem Task.CompletedTask lub Task.FromResult polega na tym, że można pominąć błędy, które pojawiają się tylko wtedy, gdy metoda wykonuje asynchronicznie.
Joakim MH