wake-up-neo.com

Task.Yield - Echte Verwendungen?

Ich habe über Task.Yield gelesen und als Javascript-Entwickler kann ich sagen, dass es der Job ist genau das gleiche wie setTimeout(function (){...},0); in Bezug darauf, dass der Haupt-Single-Thread sich mit anderen Dingen befasst, auch bekannt als:

"Nehmen Sie nicht die ganze Kraft, lassen Sie die Zeit hinter sich - andere würden auch welche haben ..."

In js funktioniert es besonders in langen Schleifen. (Browser nicht einfrieren lassen ...)

Aber ich sah dieses Beispiel hier :

public static async Task < int > FindSeriesSum(int i1)
{
    int sum = 0;
    for (int i = 0; i < i1; i++)
    {
        sum += i;
        if (i % 1000 == 0) ( after a bulk , release power to main thread)
            await Task.Yield();
    }

    return sum;
}

Als JS-Programmierer kann ich verstehen, was sie hier gemacht haben.

ABER als C # -Programmierer frage ich mich: Warum nicht eine Aufgabe dafür eröffnen?

 public static async Task < int > FindSeriesSum(int i1)
    {
         //do something....
         return await MyLongCalculationTask();
         //do something
    }

Frage

Mit Js kann ich keine Aufgabe öffnen (ja, ich weiß, ich kann tatsächlich mit Web-Arbeitern ). Aber mit c # kann ich .

Wenn ja - warum sogar ab und zu loslassen, während ich es überhaupt loslassen kann?

Bearbeiten

Hinzufügen von Referenzen:

Von hierenter image description here

Von hier (ein anderes eBook):

enter image description here

36
Royi Namir

Wenn du siehst:

await Task.Yield();

sie können auf diese Weise darüber nachdenken:

await Task.Factory.StartNew( 
    () => {}, 
    CancellationToken.None, 
    TaskCreationOptions.None, 
    SynchronizationContext.Current != null?
        TaskScheduler.FromCurrentSynchronizationContext(): 
        TaskScheduler.Current);

Dadurch wird sichergestellt, dass die Fortsetzung in der Zukunft asynchron erfolgt. Mit asynchron Ich meine, dass die Ausführungssteuerung zum Aufrufer der async-Methode zurückkehrt und der Continuation-Callback nicht auf demselben Stack-Frame ausgeführt wird.

Wann genau und auf welchem ​​Thread dies geschieht, hängt vollständig vom Synchronisationskontext des aufrufenden Threads ab.

Bei einem UI-Thread wird die Fortsetzung bei einer zukünftigen Iteration der Nachrichtenschleife ausgeführt, die von Application.Run ( WinForms ) oder Dispatcher.Run ( WPF ) ausgeführt wird. Intern kommt es auf die Win32 PostMessage-API an, die eine benutzerdefinierte Nachricht in der Nachrichtenwarteschlange des UI-Threads bereitstellt. Der Continuous Callback await wird aufgerufen, wenn diese Nachricht gepumpt und verarbeitet wird. Sie sind völlig außer Kontrolle darüber, wann genau dies passieren wird.

Außerdem hat Windows eigene Prioritäten für das Pumpen von Nachrichten: INFO: Window Message Priorities . Der relevanteste Teil:

Im Rahmen dieses Schemas kann die Priorisierung als Drei-Level betrachtet werden. Alles Gesendete Nachrichten haben eine höhere Priorität als Benutzereingabemeldungen, da Sie befinden sich in verschiedenen Warteschlangen. Und alle Benutzereingabemeldungen sind höhere Priorität als WM_Paint- und WM_TIMER-Nachrichten.

Wenn Sie also await Task.Yield() verwenden, um der Meldungsschleife nachzugeben, um zu versuchen, die Benutzeroberfläche aktiv zu halten, besteht das Risiko, dass die Meldungsschleife des UI-Threads blockiert wird. Einige ausstehende Benutzereingabenachrichten sowie WM_Paint und WM_TIMER haben eine niedrigere Priorität als die veröffentlichte Fortsetzungsnachricht. Wenn Sie also await Task.Yield() in einer engen Schleife ausführen, können Sie die Benutzeroberfläche trotzdem blockieren.

So unterscheidet es sich von der JavaScript-setTimer-Analogie, die Sie in der Frage erwähnt haben. Ein setTimer-Callback wird als after bezeichnet. Alle Benutzereingabenachrichten wurden von der Message-Pumpe des Browsers verarbeitet.

await Task.Yield() eignet sich daher nicht für Hintergrundarbeiten im UI-Thread. In der Tat müssen Sie sehr selten einen Hintergrundprozess im UI-Thread ausführen, manchmal aber auch, z. Hervorheben der Editor-Syntax, Rechtschreibprüfung usw. Verwenden Sie in diesem Fall die inaktive Infrastruktur des Frameworks.

Zum Beispiel könnten Sie mit WPF await Dispatcher.Yield(DispatcherPriority.ApplicationIdle):

async Task DoUIThreadWorkAsync(CancellationToken token)
{
    var i = 0;

    while (true)
    {
        token.ThrowIfCancellationRequested();

        await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);

        // do the UI-related work item
        this.TextBlock.Text = "iteration " + i++;
    }
}

Für WinForms können Sie das Application.Idle -Ereignis verwenden:

// await IdleYield();

public static Task IdleYield()
{
    var idleTcs = new TaskCompletionSource<bool>();
    // subscribe to Application.Idle
    EventHandler handler = null;
    handler = (s, e) =>
    {
        Application.Idle -= handler;
        idleTcs.SetResult(true);
    };
    Application.Idle += handler;
    return idleTcs.Task;
}

Es wird empfohlen , dass Sie für jede Iteration einer solchen Hintergrundoperation, die auf dem UI-Thread ausgeführt wird, nicht mehr als 50 ms dauern.

Für einen Nicht-UI-Thread ohne Synchronisationskontext wechselt await Task.Yield() die Fortsetzung einfach zu einem Pool mit zufälligen Pools. Es gibt keine Garantie dafür, dass es sich um einen anderen - Thread aus dem aktuellen Thread handelt. Es wird nur garantiert, dass es sich um eine asynchronous - Fortsetzung handelt. Wenn ThreadPool verhungert, kann die Fortsetzung in demselben Thread geplant werden.

In ASP.NET macht das Ausführen von await Task.Yield() überhaupt keinen Sinn, außer für die in @ StephenClearys Antwort erwähnte Problemumgehung. Andernfalls wird die Web-App-Leistung nur durch einen redundanten Thread-Switch beeinträchtigt.

Also, ist await Task.Yield() nützlich? IMO, nicht viel. Es kann als Abkürzung verwendet werden, um die Fortsetzung über SynchronizationContext.Post oder ThreadPool.QueueUserWorkItem auszuführen, wenn Sie wirklich einen Teil Ihrer Methode asynchronisieren müssen.

Bezüglich der von Ihnen zitierten Bücher sind diese Ansätze zur Verwendung von Task.Yield meiner Meinung nach falsch. Ich habe oben erklärt, warum sie für einen UI-Thread falsch sind. Für einen Nicht-UI-Pool-Thread gibt es einfach keine "andere Aufgaben im Thread, die ausgeführt werden sollen", es sei denn, Sie führen eine benutzerdefinierte Task-Pumpe wie Stephen Toubs AsyncPump aus. 

Aktualisiert, um den Kommentar zu beantworten:

... wie kann es asynchroner Betrieb sein und im selben Thread verbleiben ..

Als einfaches Beispiel: WinForms-App:

async void Form_Load(object s, object e) 
{ 
    await Task.Yield(); 
    MessageBox.Show("Async message!");
}

Form_Load wird an den Aufrufer zurückgegeben (der WinFroms-Framework-Code, der das Load-Ereignis ausgelöst hat). Anschließend wird das Meldungsfeld asynchron angezeigt, sobald die durch Application.Run() ausgeführte Meldungsschleife wiederholt wird. Der Continuation-Callback wird mit WinFormsSynchronizationContext.Post in eine Warteschlange gestellt, die intern eine private Windows-Nachricht an die Nachrichtenschleife des UI-Threads sendet. Der Rückruf wird ausgeführt, wenn diese Nachricht immer noch im selben Thread gepumpt wird.

In einer Konsolen-App können Sie eine ähnliche Serialisierungsschleife mit der oben genannten AsyncPump ausführen.

52
noseratio

Ich habe Task.Yield nur in zwei Szenarien als nützlich befunden:

  1. Komponententests, um sicherzustellen, dass der zu testende Code bei Vorhandensein von Asynchronität ordnungsgemäß funktioniert.
  2. So umgehen Sie ein obskures ASP.NET-Problem, bei dem der Identitätscode nicht synchron abgeschlossen werden kann .
17
Stephen Cleary

Nein, es ist nicht genau wie mit setTimeout, um die Kontrolle an die Benutzeroberfläche zurückzugeben. In Javascript würde die Aktualisierung der Benutzeroberfläche immer erfolgen, da die Variable setTimeout immer eine minimale Pause von einigen Millisekunden hat. Die ausstehende Arbeit der Benutzeroberfläche hat Vorrang vor Timern, aber await Task.Yield(); tut dies nicht.

Es gibt keine Garantie dafür, dass durch die Rendite Arbeit im Haupt-Thread ausgeführt werden kann. Im Gegensatz dazu wird der Code, der die Rendite aufgerufen hat, häufig der Arbeit der Benutzeroberfläche vorgezogen.

"Der Synchronisationskontext, der in einem UI-Thread in den meisten UI-Umgebungen vorhanden ist, priorisiert häufig die Arbeit, die in einem höheren Kontext als die Eingabe- und Rendering-Arbeit gepostet wird. Yield (); um eine Benutzeroberfläche ansprechend zu halten. "

Ref: MSDN: Task.Yield-Methode

6
Guffa

Lassen Sie mich zunächst klarstellen: Yield ist nicht genau dasselbe wie setTimeout(function (){...},0);. JS wird in einer Single-Thread-Umgebung ausgeführt. Nur so können andere Aktivitäten ausgeführt werden. Art kooperatives Multitasking . .net wird in einer präemptiven Multitasking-Umgebung mit explizitem Multithreading ausgeführt.

Nun zurück zu Thread.Yield. Wie ich sagte, lebt .net in einer präventiven Welt, aber es ist etwas komplizierter. C # await/async erstellt eine interessante Mischung aus dem Multitasking-Modus, der von Zustandsmaschinen gesteuert wird. Wenn Sie also Yield in Ihrem Code weglassen, wird der Thread einfach blockiert und das ist es. Wenn Sie es zu einer regulären Aufgabe machen und nur start (oder einen Thread) aufrufen, wird es einfach parallel ausgeführt und der aufrufende Thread wird später blockiert, wenn task.Result aufgerufen wird. Was passiert, wenn Sie await Task.Yield(); tun, ist komplizierter. Logischerweise wird der aufrufende Code (ähnlich wie bei JS) freigegeben, und die Ausführung wird fortgesetzt. Was es tatsächlich tut - es wählt einen anderen Thread aus und setzt seine Ausführung in einer präventiven Umgebung mit aufrufendem Thread fort. So ist es in aufrufendem Thread bis zuerst Task.Yield und dann ist es alleine. Nachfolgende Aufrufe von Task.Yield machen anscheinend nichts.

Einfache Demonstration:

class MainClass
{
    //Just to reduce amont of log itmes
    static HashSet<Tuple<string, int>> cache = new HashSet<Tuple<string, int>>();
    public static void LogThread(string msg, bool clear=false) {
        if (clear)
            cache.Clear ();
        var val = Tuple.Create(msg, Thread.CurrentThread.ManagedThreadId);
        if (cache.Add (val))
            Console.WriteLine ("{0}\t:{1}", val.Item1, val.Item2);
    }

    public static async Task<int> FindSeriesSum(int i1)
    {
        LogThread ("Task enter");
        int sum = 0;
        for (int i = 0; i < i1; i++)
        {
            sum += i;
            if (i % 1000 == 0) {
                LogThread ("Before yield");
                await Task.Yield ();
                LogThread ("After yield");
            }
        }
        LogThread ("Task done");
        return sum;
    }

    public static void Main (string[] args)
    {
        LogThread ("Before task");
        var task = FindSeriesSum(1000000);
        LogThread ("While task", true);
        Console.WriteLine ("Sum = {0}", task.Result);
        LogThread ("After task");
    }
}

Hier sind Ergebnisse:

Before task     :1
Task enter      :1
Before yield    :1
After yield     :5
Before yield    :5
While task      :1
Before yield    :5
After yield     :5
Task done       :5
Sum = 1783293664
After task      :1
  • Ausgabe unter Mac OS X auf Mono 4.5. Die Ergebnisse können bei anderen Einstellungen variieren

Wenn Sie Task.Yield über die Methode verschieben, wird dies asynchron von Anfang an durchgeführt und der aufrufende Thread wird nicht blockiert.

Schlussfolgerung: Task.Yield kann das Mischen von Synchronisationscode und Asynchroncode ermöglichen. Etwas mehr oder weniger realistisches Szenario: Sie haben einige Rechenoperationen und einen lokalen Cache sowie eine CalcThing-Task. Bei dieser Methode prüfen Sie, ob sich das Element im Cache befindet. Wenn ja, geben Sie das Element zurück, falls es nicht Yield vorhanden ist, und fahren Sie mit der Berechnung fort. Eigentlich ist die Probe aus Ihrem Buch ziemlich bedeutungslos, da dort nichts Nützliches erreicht wird. Ihre Bemerkung zur GUI-Interaktivität ist einfach schlecht und falsch (der UI-Thread wird bis zum ersten Aufruf von Yield gesperrt. Sie sollten dies niemals tun. MSDN ist klar (und richtig): "Verlassen Sie sich nicht auf" wait " Task.Yield (); um eine Benutzeroberfläche ansprechend zu halten ".

2
Andrey

Sie gehen davon aus, dass die Langzeitfunktion in einem Hintergrundthread ausgeführt werden kann. Wenn dies nicht der Fall ist, zum Beispiel weil es eine Interaktion mit der Benutzeroberfläche gibt, gibt es keine Möglichkeit, die Blockierung der Benutzeroberfläche während der Ausführung zu verhindern. Daher sollten die Ausführungszeiten kurz genug sein, um den Benutzern keine Probleme zu verursachen.

Eine andere Möglichkeit besteht darin, dass Sie mehr Funktionen mit langer Laufzeit als Hintergrundthreads haben. In diesem Szenario kann es besser sein (oder es spielt keine Rolle, abhängig davon), ob einige dieser Funktionen alle Ihre Threads übernehmen.

0
user743382

Ich denke, dass niemand die wirkliche Antwort gegeben hat, wann Task.Yield ..__ verwendet wird. Dies wird meistens benötigt, wenn eine Task eine unendliche Schleife (oder einen langen synchronen Job) verwendet und möglicherweise einen Threadpool-Thread ausschließlich halten kann (andere nicht zulassen Aufgaben, um diesen Thread zu verwenden). Dies kann passieren, wenn der Code innerhalb der Schleife synchron läuft. der Task.Yield-Zeitplan die Task an die Threadpool-Warteschlange und die anderen Tasks, die auf den Thread gewartet haben, können ausgeführt werden.

Das Beispiel:

  CancellationTokenSource cts;
  void Start()
  {
        cts = new CancellationTokenSource();

        // run async operation
        var task = Task.Run(() => SomeWork(cts.Token), cts.Token);
        // wait for completion
        // after the completion handle the result/ cancellation/ errors
    }

    async Task<int> SomeWork(CancellationToken cancellationToken)
    {
        int result = 0;

        bool loopAgain = true;
        while (loopAgain)
        {
            // do something ... means a substantial work or a micro batch here - not processing a single byte

            loopAgain = /* check for loop end && */  cancellationToken.IsCancellationRequested;
            if (loopAgain) {
                // reschedule  the task to the threadpool and free this thread for other waiting tasks
                await Task.Yield();
            }
        }
        cancellationToken.ThrowIfCancellationRequested();
        return result;
    }

    void Cancel()
    {
        // request cancelation
        cts.Cancel();
    }
0
Maxim T