Wann würde ich Task.Yield () verwenden?

218

Ich benutze async / await und Taskviel, habe es aber nie benutzt Task.Yield()und um ehrlich zu sein, trotz all der Erklärungen verstehe ich nicht, warum ich diese Methode brauchen würde.

Kann jemand ein gutes Beispiel geben, wo Yield()es erforderlich ist?

Krumelur
quelle

Antworten:

241

Wenn Sie async/ verwenden await, gibt es keine Garantie dafür, dass die Methode, die Sie dabei aufrufen await FooAsync(), tatsächlich asynchron ausgeführt wird. Die interne Implementierung kann über einen vollständig synchronen Pfad zurückgegeben werden.

Wenn Sie eine API erstellen, bei der es wichtig ist, dass Sie nicht blockieren und Code asynchron ausführen, und die aufgerufene Methode await Task.Yield()möglicherweise synchron ausgeführt wird (effektiv blockiert), erzwingt die Verwendung, dass Ihre Methode asynchron ist, und kehrt zurück Kontrolle an diesem Punkt. Der Rest des Codes wird zu einem späteren Zeitpunkt (zu diesem Zeitpunkt wird er möglicherweise noch synchron ausgeführt) im aktuellen Kontext ausgeführt.

Dies kann auch nützlich sein, wenn Sie eine asynchrone Methode erstellen, die eine "lang laufende" Initialisierung erfordert, z.

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

Ohne den Task.Yield()Aufruf wird die Methode bis zum ersten Aufruf von synchron ausgeführt await.

Reed Copsey
quelle
26
Ich habe das Gefühl, hier etwas falsch zu interpretieren. Wenn await Task.Yield()die Methode asynchron sein soll, warum sollten wir uns dann die Mühe machen, "echten" asynchronen Code zu schreiben? Stellen Sie sich eine schwere Synchronisationsmethode vor. Um es asynchron zu machen, fügen Sie einfach hinzu asyncund await Task.Yield()am Anfang und auf magische Weise wird es asynchron sein? Das wäre so ziemlich so, als würde man den gesamten Synchronisierungscode einpacken Task.Run()und eine gefälschte asynchrone Methode erstellen.
Krumelur
14
@Krumelur Es gibt einen großen Unterschied - schauen Sie sich mein Beispiel an. Wenn Sie ein verwenden Task.Run, um es zu implementieren, ExecuteFooOnUIThreadwird es im Thread-Pool ausgeführt, nicht im UI-Thread. Mit await Task.Yield()erzwingen Sie, dass es so asynchron ist, dass der nachfolgende Code immer noch im aktuellen Kontext ausgeführt wird (nur zu einem späteren Zeitpunkt). Es ist nicht etwas, was Sie normalerweise tun würden, aber es ist schön, dass es die Option gibt, wenn es aus irgendeinem seltsamen Grund erforderlich ist.
Reed Copsey
7
Noch eine Frage: Wenn ExecuteFooOnUIThread()es sehr lange laufen würde, würde es den UI-Thread irgendwann noch für eine lange Zeit blockieren und die UI nicht mehr reagieren lassen. Ist das richtig?
Krumelur
7
@Krumelur Ja, das würde es. Nur nicht sofort - es würde zu einem späteren Zeitpunkt passieren.
Reed Copsey
33
Obwohl diese Antwort technisch korrekt ist, ist die Aussage, dass "der Rest des Codes zu einem späteren Zeitpunkt ausgeführt wird", zu abstrakt und möglicherweise irreführend. Der Ausführungsplan des Codes nach Task.Yield () hängt stark vom konkreten SynchronisationContext ab. In der MSDN-Dokumentation heißt es eindeutig: "Der Synchronisationskontext, der in den meisten UI-Umgebungen in einem UI-Thread vorhanden ist, priorisiert häufig Arbeiten, die im Kontext veröffentlicht werden, höher als Eingabe- und Rendering-Arbeiten. Verlassen Sie sich aus diesem Grund nicht darauf, Task.Yield () abzuwarten." ; um eine Benutzeroberfläche ansprechbar zu halten. "
Vitaliy Tsvayer
36

Intern müssen Sie await Task.Yield()die Fortsetzung einfach entweder im aktuellen Synchronisationskontext oder in einem zufälligen Pool-Thread in die Warteschlange stellen, falls dies der Fall SynchronizationContext.Currentist null.

Es wird effizient als benutzerdefinierter Kellner implementiert. Ein weniger effizienter Code, der den identischen Effekt erzeugt, könnte so einfach sein:

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()kann als Abkürzung für einige seltsame Änderungen des Ausführungsflusses verwendet werden. Beispielsweise:

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  
}

Das heißt, ich kann mir keinen Fall Task.Yield()vorstellen, in dem nicht durch einen Task.Factory.StartNewrichtigen Taskplaner ersetzt werden kann.

Siehe auch:

noseratio
quelle
Was ist in Ihrem Beispiel der Unterschied zwischen dem, was da ist und var dialogTask = await showAsync();?
Erik Philips
@ErikPhilips, var dialogTask = await showAsync()wird nicht kompiliert, da der await showAsync()Ausdruck kein a zurückgibt Task(im Gegensatz zu ohne await). Wenn Sie dies jedoch tun await showAsync(), wird die Ausführung nach dem Schließen des Dialogfelds erst wieder aufgenommen. So ist es anders. Das liegt daran, dass window.ShowDialoges sich um eine synchrone API handelt (obwohl sie immer noch Nachrichten pumpt). In diesem Code wollte ich fortfahren, während der Dialog noch angezeigt wird.
Noseratio
5

Eine Verwendung Task.Yield()besteht darin, einen Stapelüberlauf zu verhindern, wenn eine asynchrone Rekursion durchgeführt wird. Task.Yield()verhindert synchrone Fortsetzung. Beachten Sie jedoch, dass dies eine OutOfMemory-Ausnahme verursachen kann (wie von Triynko angegeben). Endlose Rekursion ist immer noch nicht sicher und Sie sollten die Rekursion wahrscheinlich besser als Schleife umschreiben.

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
quelle
4
Dies kann einen Stapelüberlauf verhindern, aber irgendwann wird der Systemspeicher knapp, wenn Sie ihn lange genug laufen lassen. Jede Iteration würde eine neue Aufgabe erstellen, die niemals abgeschlossen wird, da die äußere Aufgabe auf eine innere Aufgabe wartet, die auf eine weitere innere Aufgabe wartet, und so weiter. Das ist nicht in Ordnung. Alternativ können Sie einfach eine äußerste Aufgabe haben, die niemals abgeschlossen wird, und sie einfach schleifen lassen, anstatt sie zu wiederholen. Die Aufgabe würde niemals abgeschlossen werden, aber es würde nur einen von ihnen geben. Innerhalb der Schleife könnte es nachgeben oder auf alles warten, was Sie möchten.
Triynko
Ich kann den Stapelüberlauf nicht reproduzieren. Es scheint, dass das await Task.Delay(1)ausreicht, um es zu verhindern. (Konsolen-App, .NET Core 3.1, C # 8)
Theodor Zoulias
-8

Task.Yield() kann in Scheinimplementierungen von asynchronen Methoden verwendet werden.

mhsirig
quelle
4
Sie sollten einige Details angeben.
PJProudhon
3
Zu diesem Zweck würde ich lieber Task.CompletedTask verwenden - weitere Überlegungen finden Sie im Abschnitt Task.CompletedTask in diesem msdn-Blogbeitrag .
Grzegorz Smulko
2
Das Problem bei der Verwendung von Task.CompletedTask oder Task.FromResult besteht darin, dass Sie Fehler übersehen können, die nur auftreten, wenn die Methode asynchron ausgeführt wird.
Joakim MH