wake-up-neo.com

Stimmt es, dass Async nicht für Aufgaben mit hoher CPU-Auslastung verwendet werden sollte?

Ich habe mich gefragt, ob es stimmt, dass async-await nicht für Aufgaben mit "hoher CPU" verwendet werden sollte. Ich habe das behauptet in einer Präsentation gesehen.

Ich denke, das würde so etwas bedeuten

Task<int> calculateMillionthPrimeNumber = CalculateMillionthPrimeNumberAsync();
DoIndependentWork();
int p = await calculateMillionthPrimeNumber;

Meine Frage ist könnte das oben Genannte gerechtfertigt sein, oder gibt es, wenn nicht, ein anderes Beispiel für die asynchrone Ausführung einer Aufgabe mit hoher CPU?

22
user7127000

Ich habe mich gefragt, ob es wahr ist, dass async-await nicht für Aufgaben mit "hoher CPU" verwendet werden sollte.

Ja das stimmt.

Ich frage mich, ob dies gerechtfertigt sein könnte

Ich würde sagen, dass es nicht gerechtfertigt ist. Im Allgemeinen sollten Sie Task.Run nicht zur Implementierung von Methoden mit asynchronen Signaturen verwenden. Keine asynchronen Wrapper für synchrone Methoden verfügbar machen . Dies soll Verwirrung durch Verbraucher, insbesondere bei ASP.NET, verhindern.

Die Verwendung von Task.Run für call einer synchronen Methode, z. B. in einer UI-App, ist jedoch nicht falsch. Auf diese Weise können Sie Multithreading (Task.Run) verwenden, um den UI-Thread freizuhalten und elegant mit await zu konsumieren:

var task = Task.Run(() => CalculateMillionthPrimeNumber());
DoIndependentWork();
var prime = await task;
7
Stephen Cleary

In der Tat gibt es zwei Hauptanwendungen von Async/Erwartung. Einer (und ich verstehe, dass dies einer der Hauptgründe ist, warum es in das Framework aufgenommen wurde) ist, dem aufrufenden Thread andere Aufgaben zu ermöglichen, während er auf ein Ergebnis wartet. Dies ist meistens für E/A-gebundene Aufgaben (d. H. Aufgaben, bei denen der Haupt-"Holdup" eine Art von E/A ist - Warten darauf, dass eine Festplatte, ein Server, ein Drucker usw. ihre Aufgabe beantwortet oder abschließt).

Als Randbemerkung: Wenn Sie async/await auf diese Weise verwenden, müssen Sie sicherstellen, dass Sie die Implementierung so durchführen, dass der aufrufende Thread andere Aufgaben erledigen kann, während er auf das Ergebnis wartet. Ich habe viele Fälle gesehen, in denen Leute Sachen wie "A wartet auf B, was auf C wartet" machen; Dies kann am Ende nicht besser sein, als wenn A nur B synchron aufgerufen hat und B nur C synchron aufgerufen hat (da der aufrufende Thread niemals andere Aufgaben ausführen darf, während er auf die Ergebnisse von B und C wartet).

Bei E/A-gebundenen Aufgaben ist es nicht sinnvoll, einen zusätzlichen Thread zu erstellen, nur um auf ein Ergebnis zu warten. Meine übliche Analogie hier ist, in einem Restaurant mit 10 Personen in einer Gruppe zu bestellen. Wenn die erste Person, die der Kellner zur Bestellung auffordert, noch nicht fertig ist, wartet der Kellner nicht nur darauf, dass er bereit ist, bevor er die Bestellung eines anderen Auftragnehmers annimmt, noch bringt er einen zweiten Kellner mit, nur um auf den ersten Mann zu warten. In diesem Fall sollten Sie die anderen 9 Personen in der Gruppe nach ihren Befehlen fragen. hoffentlich wird der erste Kerl zum Zeitpunkt der Bestellung bereit sein. Wenn nicht, hat der Kellner immer noch etwas Zeit gespart, weil er weniger Zeit damit verbringt, untätig zu bleiben.

Es ist auch möglich, Dinge wie Task.Run für CPU-gebundene Aufgaben zu verwenden (und dies ist die zweite Verwendung dafür). Um unserer obigen Analogie zu folgen, ist dies ein Fall, in dem es im Allgemeinen nützlich wäre, mehr Kellner zu haben - z. wenn es zu viele Tische für einen Kellner gab. Alles, was dies tatsächlich "hinter den Kulissen" tut, ist der Thread-Pool. Es ist eines von mehreren möglichen Konstrukten, um CPU-gebundene Arbeit auszuführen (z. B. einfach "direkt" in den Thread-Pool zu legen, explizit einen neuen Thread zu erstellen oder einen Background Worker zu verwenden). Es ist also eine Design-Frage, welchen Mechanismus Sie enden mit verwenden.

Ein Vorteil von async/await ist hier, dass er (unter den richtigen Umständen) die Menge an expliziter Verriegelungs-/Synchronisationslogik reduzieren kann, die Sie manuell schreiben müssen. Hier ist eine Art dummes Beispiel:

private static async Task SomeCPUBoundTask()
    {
        // Insert actual CPU-bound task here using Task.Run
        await Task.Delay(100);
    }

    public static async Task QueueCPUBoundTasks()
    {
        List<Task> tasks = new List<Task>();

        // Queue up however many CPU-bound tasks you want
        for (int i = 0; i < 10; i++)
        {
            // We could just call Task.Run(...) directly here
            Task task = SomeCPUBoundTask();

            tasks.Add(task);
        }

        // Wait for all of them to complete
        // Note that I don't have to write any explicit locking logic here,
        // I just tell the framework to wait for all of them to complete
        await Task.WhenAll(tasks);
    }

Ich gehe natürlich davon aus, dass die Aufgaben vollständig parallelisierbar sind. Beachten Sie auch, dass Sie könnten den Thread-Pool hier selbst verwendet haben, aber das wäre weniger bequem, da Sie einen Weg benötigen, um herauszufinden, ob alle von ihnen abgeschlossen wurden (und nicht nur.) Lassen Sie den Rahmen das für Sie herausfinden). Möglicherweise konnten Sie hier auch eine Parallel.For-Schleife verwenden.

8
EJoshuaS

Nehmen wir an, Ihr CalculateMillionthPrimeNumberwar so etwas wie das Folgende (nicht sehr effizient oder ideal bei der Verwendung von gotoname__, aber sehr einfach zu verstehen):

public int CalculateMillionthPrimeNumber()
{
    List<int> primes = new List<int>(1000000){2};
    int num = 3;
    while(primes.Count < 1000000)
    {
        foreach(int div in primes)
        {
            if ((num / div) * div == num)
                goto next;
        }
        primes.Add(num);
        next:
            ++num;
    }
    return primes.Last();
}

Nun gibt es keinen sinnvollen Punkt, an dem dies asynchron etwas bewirken kann. Lassen Sie uns daraus eine Task-returning-Methode mit asyncmachen:

public async Task<int> CalculateMillionthPrimeNumberAsync()
{
    List<int> primes = new List<int>(1000000){2};
    int num = 3;
    while(primes.Count < 1000000)
    {
        foreach(int div in primes)
        {
            if ((num / div) * div == num)
                goto next;
        }
        primes.Add(num);
        next:
            ++num;
    }
    return primes.Last();
}

Der Compiler wird uns davor warnen, weil awaitnix nützlich ist. Wenn Sie dies wirklich aufrufen, wird dies eine etwas kompliziertere Version des Aufrufs von Task.FromResult(CalculateMillionthPrimeNumber()) sein. Das heißt, es ist das gleiche wie die Berechnung und then Erstellen einer bereits abgeschlossenen Aufgabe, die die berechnete Zahl als Ergebnis hat.

Nun sind bereits erledigte Aufgaben nicht immer sinnlos. Betrachten Sie zum Beispiel:

public async Task<string> GetInterestingStringAsync()
{
    if (_cachedInterestingString == null)
      _cachedInterestingString = await GetStringFromWeb();
    return _cachedInterestingString;
}

Dies gibt eine bereits abgeschlossene Aufgabe zurück, wenn sich die Zeichenfolge im Cache befindet und nicht anders. In diesem Fall wird sie ziemlich schnell zurückgegeben. Andere Fälle liegen vor, wenn mehrere Implementierungen derselben Schnittstelle vorhanden sind und nicht alle Implementierungen asynchrone E/A verwenden können.

Und ebenfalls eine asyncname__-Methode, bei der awaitname__s diese Methode eine bereits abgeschlossene Aufgabe zurückgibt oder nicht, abhängig davon. Es ist eigentlich eine großartige Möglichkeit, einfach im selben Thread zu bleiben und zu tun, was getan werden muss, wenn dies möglich ist.

Wenn jedoch always möglich ist, besteht der einzige Effekt darin, dass das Taskname__-Objekt und der Zustandscomputer, den asyncverwendet, zum Erstellen des Objekts verwendet werden.

Also ziemlich sinnlos. Wenn die Version in Ihrer Frage so implementiert wurde, hätte calculateMillionthPrimeNumberIsCompletedvon Anfang an true zurückgegeben. Sie sollten nur die nicht asynchrone Version aufgerufen haben.

Okay, als Implementierer von CalculateMillionthPrimeNumberAsync() wollen wir etwas Nützlicheres für unsere Benutzer tun. So machen wir es:

public Task<int> CalculateMillionthPrimeNumberAsync()
{
    return Task.Factory.StartNew(CalculateMillionthPrimeNumber, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
}

Okay, jetzt verschwenden wir keine Zeit für unsere Benutzer. DoIndependentWork() wird gleichzeitig mit CalculateMillionthPrimeNumberAsync() Sachen erledigen, und wenn es zuerst beendet wird, gibt der awaitdiesen Thread frei.

Großartig!

Nur haben wir die Nadel nicht wirklich aus der synchronen Position bewegt. In der Tat, besonders wenn DoIndependentWork() nicht anstrengend ist, haben wir es vielleicht noch viel schlimmer gemacht. Die synchrone Methode würde alles in einem Thread erledigen, nennen wir es Thread A. Die neue Methode führt die Berechnung auf Thread B aus, gibt dann entweder Thread A frei und synchronisiert sich dann auf verschiedene Weise. Es ist viel Arbeit, hat es etwas gebracht?

Nun, vielleicht, aber der Autor von CalculateMillionthPrimeNumberAsync() kann das nicht wissen, weil die Faktoren, die den Aufruf beeinflussen, alle im aufrufenden Code stehen. Der aufrufende Code könnte StartNewselbst ausgeführt haben und hätte die Synchronisationsoptionen besser an die Anforderungen anpassen können, wenn dies der Fall war.

Tasks können zwar ein bequemer Weg sein, um cpu-gebundenen Code parallel zu einem anderen Task aufzurufen, Methoden, die dies tun, sind jedoch nicht nützlich. Schlimmer noch, sie täuschen, als jemand, der CalculateMillionthPrimeNumberAsyncsieht, kann man glauben, dass der Aufruf nicht sinnlos ist.

4
Jon Hanna

Wenn CalculateMillionthPrimeNumberAsync nicht ständig async/await allein verwendet, gibt es keinen Grund, die Task nicht mit hoher CPU-Leistung arbeiten zu lassen, da sie nur Ihre Methode an den ThreadPool-Thread delegiert.

Was ist ein ThreadPool-Thread und wie unterscheidet er sich von einem regulären Thread hier .

Kurz gesagt, der Threadpool-Thread wird nur für einige Zeit in Verwahrung genommen (und die Anzahl der Threadpool-Threads ist begrenzt). Wenn Sie also nicht zu viele Threads verwenden, müssen Sie sich keine Sorgen machen.

1
AgentFire