427 Stimmen

Wann man Task.Run korrekt einsetzt und wann man einfach async-await verwendet

Ich würde gerne Ihre Meinung zur korrekten Architektur bei der Verwendung von Task.Run hören. Ich habe eine verzögerte Benutzeroberfläche in unserer WPF .NET 4.5-Anwendung (mit dem Caliburn Micro Framework).

Grundsätzlich mache ich (sehr vereinfachte Code-Schnipsel):

public class PageViewModel : IHandle
{
   ...

   public async void Handle(SomeMessage message)
   {
      ZeigeLadeanimation();

      // Macht die Benutzeroberfläche sehr träge, aber noch nicht tot
      await this.contentLoader.LoadContentAsync();

      VersteckeLadeanimation();
   }
}

public class ContentLoader
{
    public async Task LoadContentAsync()
    {
        await DoCpuBoundWorkAsync();
        await DoIoBoundWorkAsync();
        await DoCpuBoundWorkAsync();

        // Ich bin nicht wirklich sicher, was alles als CPU-gebunden angesehen werden kann, was die Benutzeroberfläche verlangsamt
        await DoSomeOtherWorkAsync();
    }
}

Von den Artikeln/Videos, die ich gelesen/gesehen habe, weiß ich, dass await async nicht unbedingt auf einem Hintergrundthread ausgeführt wird und um die Arbeit im Hintergrund zu starten, muss sie mit Task.Run(async () => ... ) umschlossen werden. Die Verwendung von async await blockiert die Benutzeroberfläche nicht, aber sie wird immer noch auf dem UI-Thread ausgeführt, was sie träge macht.

Wo ist der beste Ort, um Task.Run zu platzieren?

Sollte ich einfach

  1. den äußeren Aufruf umschließen, da dies für .NET weniger Threading-Arbeit ist?

  2. , oder sollte ich nur CPU-gebundene Methoden intern mit Task.Run ausführen, da dies es für andere Stellen wiederverwendbar macht? Ich bin mir nicht sicher, ob es eine gute Idee ist, Arbeit auf Hintergrundthreads tief im Kern zu starten.

Ad (1), die erste Lösung wäre folgendermaßen:

public async void Handle(SomeMessage message)
{
    ZeigeLadeanimation();
    await Task.Run(async () => await this.contentLoader.LoadContentAsync());
    VersteckeLadeanimation();
}

// Andere Methoden verwenden kein Task.Run, da jetzt alles unabhängig
// davon, ob es E/A-gebunden oder CPU-gebunden ist, im Hintergrund ausgeführt wird.

Ad (2), die zweite Lösung wäre folgendermaßen:

public async Task DoCpuBoundWorkAsync()
{
    await Task.Run(() => {
        // Führe hier viel Arbeit aus
    });
}

public async Task DoSomeOtherWorkAsync(
{
    // Ich bin mir nicht sicher, wie ich diese Methoden behandeln soll -
    // wahrscheinlich muss ich eine nach der anderen testen, ob sie die Benutzeroberfläche verlangsamen
}

481voto

Stephen Cleary Punkte 402664

Beachten Sie die Leitlinien für die Ausführung von Arbeiten auf einem UI-Thread, gesammelt auf meinem Blog:

  • Blockieren Sie den UI-Thread nicht länger als 50 ms pro Mal.
  • Sie können ~100 Fortsetzungen pro Sekunde auf dem UI-Thread planen; 1000 sind zu viel.

Es gibt zwei Techniken, die Sie anwenden sollten:

1) Verwenden Sie ConfigureAwait(false), wenn Sie können.

Zum Beispiel await MyAsync().ConfigureAwait(false); anstelle von await MyAsync();.

ConfigureAwait(false) teilt dem await mit, dass Sie nicht auf dem aktuellen Kontext fortsetzen müssen (in diesem Fall bedeutet "auf dem aktuellen Kontext" "auf dem UI-Thread"). Allerdings können Sie für den Rest dieser async-Methode (nach dem ConfigureAwait) nichts tun, was voraussetzt, dass Sie sich im aktuellen Kontext befinden (z. B. UI-Elemente aktualisieren).

Weitere Informationen finden Sie in meinem MSDN-Artikel Best Practices in der asynchronen Programmierung.

2) Verwenden Sie Task.Run zur Ausführung von CPU-gebundenen Methoden.

Sie sollten Task.Run verwenden, jedoch nicht in einem Code, den Sie wiederverwenden möchten (d. h. in Bibliothekscode). Verwenden Sie also Task.Run, um die Methode aufzurufen, nicht als Teil der Implementierung der Methode.

Reine CPU-gebundene Arbeiten würden folgendermaßen aussehen:

// Dokumentation: Diese Methode ist CPU-gebunden.
void DoWork();

Die Sie mit Task.Run aufrufen würden:

await Task.Run(() => DoWork());

Methoden, die eine Mischung aus CPU-gebundenen und I/O-gebundenen Aufgaben sind, sollten eine Async-Signatur mit einer Dokumentation haben, die auf ihre CPU-gebundene Natur hinweist:

// Dokumentation: Diese Methode ist CPU-gebunden.
Task DoWorkAsync();

Die Sie auch mit Task.Run aufrufen würden (da sie teilweise CPU-gebunden ist):

await Task.Run(() => DoWorkAsync());

22voto

Paul Hatcher Punkte 6026

Ein Problem mit Ihrem ContentLoader ist, dass er intern sequenziell arbeitet. Ein besseres Muster ist, die Arbeit zu parallelisieren und am Ende zu synchronisieren, so dass wir erhalten

public class PageViewModel : IHandle
{
   ...

   public async void Handle(SomeMessage message)
   {
      ZeigeLadeanimation();

      // macht die UI sehr träge, aber immer noch nicht tot
      await this.contentLoader.LoadContentAsync(); 

      VersteckeLadeanimation();   
   }
}

public class ContentLoader 
{
    public async Task LoadContentAsync()
    {
        var tasks = new List();
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoIoBoundWorkAsync());
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoSomeOtherWorkAsync());

        await Task.WhenAll(tasks).ConfigureAwait(false);
    }
}

Offensichtlich funktioniert dies nicht, wenn eine der Aufgaben Daten von anderen früheren Aufgaben erfordert, aber sollte Ihnen insgesamt eine bessere Leistung für die meisten Szenarien bieten.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X