Wenn Sie Task.Run richtig verwenden und nur asynchron warten

317

Ich möchte Sie nach Ihrer Meinung zur richtigen Architektur fragen Task.Run. In unserer WPF .NET 4.5-Anwendung (mit Caliburn Micro Framework) tritt eine verzögerte Benutzeroberfläche auf.

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

public class PageViewModel : IHandle<SomeMessage>
{
   ...

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

      // Makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync();

      HideLoadingAnimation();
   }
}

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

        // I am not really sure what all I can consider as CPU bound as slowing down the UI
        await DoSomeOtherWorkAsync();
    }
}

Aus den Artikeln / Videos, die ich gelesen / gesehen habe, weiß ich, dass sie await asyncnicht unbedingt in einem Hintergrund-Thread ausgeführt werden. Um mit der Arbeit im Hintergrund zu beginnen, müssen Sie sie mit Warten umhüllen Task.Run(async () => ... ). Die Verwendung async awaitblockiert die Benutzeroberfläche nicht, wird jedoch im UI-Thread ausgeführt, sodass sie verzögert wird.

Wo kann Task.Run am besten platziert werden?

Soll ich nur

  1. Schließen Sie den äußeren Aufruf ab, da dies für .NET weniger Threading-Arbeit bedeutet

  2. , oder sollte ich nur CPU-gebundene Methoden einbinden, die intern ausgeführt werden, Task.Runda dies die Wiederverwendung für andere Orte ermöglicht? Ich bin mir hier nicht sicher, ob es eine gute Idee ist, mit der Arbeit an Hintergrundthreads tief im Kern zu beginnen.

Anzeige (1), die erste Lösung wäre wie folgt:

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

// Other methods do not use Task.Run as everything regardless
// if I/O or CPU bound would now run in the background.

Anzeige (2), die zweite Lösung wäre wie folgt:

public async Task DoCpuBoundWorkAsync()
{
    await Task.Run(() => {
        // Do lot of work here
    });
}

public async Task DoSomeOtherWorkAsync(
{
    // I am not sure how to handle this methods -
    // probably need to test one by one, if it is slowing down UI
}
Lukas K.
quelle
Übrigens sollte die Zeile in (1) await Task.Run(async () => await this.contentLoader.LoadContentAsync());einfach sein await Task.Run( () => this.contentLoader.LoadContentAsync() );. AFAIK Sie gewinnen nichts, indem Sie eine Sekunde awaitund asyncinnen hinzufügen Task.Run. Und da Sie keine Parameter übergeben, vereinfacht sich dies etwas mehr await Task.Run( this.contentLoader.LoadContentAsync );.
ToolmakerSteve
Es gibt tatsächlich einen kleinen Unterschied, wenn Sie eine zweite Wartezeit im Inneren haben. Siehe diesen Artikel . Ich fand es sehr nützlich, nur mit diesem speziellen Punkt stimme ich nicht zu und ziehe es vor, direkt zurückzukehren, anstatt zu warten. (wie Sie in Ihrem Kommentar vorschlagen)
Lukas K

Antworten:

365

Beachten Sie die Richtlinien für die Arbeit an einem UI-Thread , die in meinem Blog gesammelt wurden:

  • Blockieren Sie den UI-Thread nicht länger als 50 ms gleichzeitig.
  • Sie können ~ 100 Fortsetzungen im UI-Thread pro Sekunde planen. 1000 ist zu viel.

Es gibt zwei Techniken, die Sie verwenden sollten:

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

ZB await MyAsync().ConfigureAwait(false);statt await MyAsync();.

ConfigureAwait(false)teilt mit, awaitdass Sie im aktuellen Kontext nicht fortfahren müssen (in diesem Fall bedeutet "im aktuellen Kontext" "im UI-Thread"). Für den Rest dieser asyncMethode (nach dem ConfigureAwait) können Sie jedoch nichts tun, was davon ausgeht, dass Sie sich im aktuellen Kontext befinden (z. B. UI-Elemente aktualisieren).

Weitere Informationen finden Sie in meinem MSDN-Artikel Best Practices in Asynchronous Programming .

2) Task.RunZum Aufrufen von CPU-gebundenen Methoden.

Sie sollten Task.RunCode verwenden , jedoch nicht in einem Code, den Sie wiederverwenden möchten (dh Bibliothekscode). So verwenden Sie rufen Sie die Methode, nicht im Rahmen der Durchführung des Verfahrens.Task.Run

Eine rein CPU-gebundene Arbeit würde also so aussehen:

// Documentation: This method is CPU-bound.
void DoWork();

Was Sie anrufen würden mit Task.Run:

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

Methoden, die eine Mischung aus CPU-gebunden und E / A-gebunden sind, sollten eine AsyncSignatur mit einer Dokumentation haben, die auf ihre CPU-gebundene Natur hinweist:

// Documentation: This method is CPU-bound.
Task DoWorkAsync();

Was Sie auch mit verwenden würden Task.Run(da es teilweise CPU-gebunden ist):

await Task.Run(() => DoWorkAsync());
Stephen Cleary
quelle
4
Danke für deine schnelle Antwort! Ich kenne den Link, den Sie gepostet haben, und habe die Videos gesehen, auf die in Ihrem Blog verwiesen wird. Eigentlich habe ich deshalb diese Frage gestellt - im Video heißt es (wie in Ihrer Antwort), Sie sollten Task.Run nicht im Kerncode verwenden. Mein Problem ist jedoch, dass ich diese Methode jedes Mal umbrechen muss, wenn ich sie verwende, um die Reaktionszeiten nicht zu verlangsamen (bitte beachten Sie, dass mein gesamter Code asynchron ist und nicht blockiert, aber ohne Thread.Run ist er nur verzögert). Ich bin auch verwirrt, ob es besser ist, nur die CPU-gebundenen Methoden (viele Task.Run-Aufrufe) oder alles in einem Task.Run vollständig zu verpacken.
Lukas K
12
Alle Ihre Bibliotheksmethoden sollten verwendet werden ConfigureAwait(false). Wenn Sie dies zuerst tun, ist dies möglicherweise Task.Runvöllig unnötig. Wenn Sie dies noch benötigen Task.Run, spielt es für die Laufzeit in diesem Fall keine große Rolle, ob Sie es einmal oder mehrmals aufrufen. Tun Sie also einfach das, was für Ihren Code am natürlichsten ist.
Stephen Cleary
2
Ich verstehe nicht, wie die erste Technik ihm helfen wird. Selbst wenn Sie ConfigureAwait(false)Ihre CPU-gebundene Methode verwenden, ist es immer noch der UI-Thread, der die CPU-gebundene Methode ausführt, und möglicherweise wird danach nur noch alles in einem TP-Thread ausgeführt. Oder habe ich etwas falsch verstanden?
Darius
4
@ user4205580: Nein, Task.Runversteht asynchrone Signaturen und wird daher erst abgeschlossen, wenn sie DoWorkAsyncabgeschlossen sind. Das extra async/ awaitist unnötig. Ich erkläre mehr über das "Warum" in meiner Blogserie über Task.RunEtikette .
Stephen Cleary
3
@ user4205580: Nein. Die überwiegende Mehrheit der asynchronen "Kern" -Methoden verwendet sie nicht intern. Der normale Weg, um eine "Kern" -asynchrone Methode zu implementieren, ist die Verwendung TaskCompletionSource<T>einer ihrer Kurzschreibweisen wie z FromAsync. Ich habe einen Blog-Beitrag, der ausführlicher beschreibt, warum für asynchrone Methoden keine Threads erforderlich sind .
Stephen Cleary
12

Ein Problem mit Ihrem ContentLoader ist, dass er intern nacheinander ausgeführt wird. Ein besseres Muster besteht darin, die Arbeit zu parallelisieren und am Ende zu synchronisieren, damit wir erhalten

public class PageViewModel : IHandle<SomeMessage>
{
   ...

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

      // makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync(); 

      HideLoadingAnimation();   
   }
}

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

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

Dies funktioniert natürlich nicht, wenn für eine der Aufgaben Daten von anderen früheren Aufgaben erforderlich sind, sollte jedoch für die meisten Szenarien ein besserer Gesamtdurchsatz erzielt werden.

Paul Hatcher
quelle