Müssen Sie Task.Run in eine Methode einfügen, um es asynchron zu machen?

304

Ich versuche, Async Wait in der einfachsten Form zu verstehen. Ich möchte eine sehr einfache Methode erstellen, die für dieses Beispiel zwei Zahlen hinzufügt. Zugegeben, es ist überhaupt keine Verarbeitungszeit, es geht nur darum, hier ein Beispiel zu formulieren.

Beispiel 1

private async Task DoWork1Async()
{
    int result = 1 + 2;
}

Beispiel 2

private async Task DoWork2Async()
{
    Task.Run( () =>
    {
        int result = 1 + 2;
    });
}

Wenn ich warte, DoWork1Async()wird der Code synchron oder asynchron ausgeführt?

Muss ich den Synchronisierungscode Task.Runumbrechen, damit die Methode abwartet UND asynchron ist, um den UI-Thread nicht zu blockieren?

Ich versuche herauszufinden, ob meine Methode eine ist Taskoder zurückgibt. Task<T>Muss ich den Code umschließen Task.Run, um ihn asynchron zu machen?

Dumme Frage Ich bin mir sicher, aber ich sehe Beispiele im Internet, in denen Leute auf Code warten, der nichts Asynchrones enthält und nicht in ein Task.Runoder eingeschlossen ist StartNew.

Neal
quelle
30
Gibt Ihnen Ihr erster Ausschnitt keine Warnungen?
Svick

Antworten:

587

Lassen Sie uns zunächst einige Begriffe klären: "asynchron" ( async) bedeutet, dass der aufrufende Thread möglicherweise vor dem Start wieder kontrolliert wird. In einer asyncMethode sind diese "Fließpunkte" awaitAusdrücke.

Dies unterscheidet sich stark von dem Begriff "asynchron", der in der MSDN-Dokumentation seit Jahren (falsch) als "auf einem Hintergrundthread ausgeführt" verwendet wird.

Das Thema weiter zu verwirren, asyncist ganz anders als "erwartbar"; Es gibt einige asyncMethoden, deren Rückgabetypen nicht erwartet werden können, und viele Methoden, die erwartete Typen zurückgeben, die nicht erwartet werden async.

Genug davon, was sie nicht sind ; Hier ist was sie sind :

  • Das asyncSchlüsselwort erlaubt eine asynchrone Methode (dh es erlaubt awaitAusdrücke). asyncMethoden können zurückgeben Task, Task<T>oder (wenn Sie müssen) void.
  • Jeder Typ, der einem bestimmten Muster folgt, kann erwartet werden. Die am häufigsten erwarteten Typen sind Taskund Task<T>.

Wenn wir Ihre Frage so umformulieren, dass "Wie kann ich eine Operation für einen Hintergrundthread so ausführen, dass sie erwartet wird", lautet die Antwort Task.Run:

private Task<int> DoWorkAsync() // No async because the method does not need await
{
  return Task.Run(() =>
  {
    return 1 + 2;
  });
}

(Aber dieses Muster ist ein schlechter Ansatz; siehe unten).

Wenn Ihre Frage jedoch lautet: "Wie erstelle ich eine asyncMethode, die dem Aufrufer zurückgeben kann, anstatt sie zu blockieren?", Lautet die Antwort, die Methode zu deklarieren asyncund awaitfür ihre "Nachgiebigkeitspunkte" zu verwenden:

private async Task<int> GetWebPageHtmlSizeAsync()
{
  var client = new HttpClient();
  var html = await client.GetAsync("http://www.example.com/");
  return html.Length;
}

Das Grundmuster der Dinge besteht also darin, dass asyncCode in seinen awaitAusdrücken von "Wartbaren" abhängt . Diese "Wartezeiten" können andere asyncMethoden oder nur reguläre Methoden sein, die Wartezeiten zurückgeben. Reguläre Methoden zurückzukehr Task/ Task<T> können verwenden Task.RunCode auf einem Hintergrund - Thread auszuführen, oder (häufiger) kann sie verwenden , TaskCompletionSource<T>oder eines ihrer Verknüpfungen ( TaskFactory.FromAsync, Task.FromResultusw.). Ich empfehle nicht , eine ganze Methode Task.Runeinzuschließen. Synchrone Methoden sollten synchrone Signaturen haben, und es sollte dem Verbraucher überlassen bleiben, ob sie in Folgendes verpackt werden sollen Task.Run:

private int DoWork()
{
  return 1 + 2;
}

private void MoreSynchronousProcessing()
{
  // Execute it directly (synchronously), since we are also a synchronous method.
  var result = DoWork();
  ...
}

private async Task DoVariousThingsFromTheUIThreadAsync()
{
  // I have a bunch of async work to do, and I am executed on the UI thread.
  var result = await Task.Run(() => DoWork());
  ...
}

Ich habe ein async/ awaitIntro in meinem Blog. Am Ende stehen einige gute Follow-up-Ressourcen. Auch die MSDN-Dokumente für asyncsind ungewöhnlich gut.

Stephen Cleary
quelle
8
@sgnsajgon: Ja. asyncMethoden müssen zurückkehren Task, Task<T>oder void. Taskund Task<T>sind zu erwarten; voidist nicht.
Stephen Cleary
3
Tatsächlich wird eine async voidMethodensignatur kompiliert, es ist nur eine ziemlich schreckliche Idee, wenn Sie Ihren Zeiger auf Ihre asynchrone Aufgabe
verlieren
4
@ TopinFrassi: Ja, sie werden kompiliert, aber voidnicht erwartet.
Stephen Cleary
4
@ohadinho: Nein, ich spreche im Blog-Beitrag davon, wenn die gesamte Methode nur ein Aufruf von ist Task.Run(wie DoWorkAsyncin dieser Antwort). Die Verwendung Task.Runzum Aufrufen einer Methode aus einem UI-Kontext ist angemessen (wie DoVariousThingsFromTheUIThreadAsync).
Stephen Cleary
2
Ja genau. Es ist gültig zu verwenden rufen eine Methode, aber wenn es eine ist um alle (oder fast alle) des Codes des Verfahrens, so dass ein Anti-Muster ist - nur halten diese Methode synchron und bewegen eine Ebene nach oben. Task.RunTask.RunTask.Run
Stephen Cleary
22

Eines der wichtigsten Dinge, die Sie beim Dekorieren einer Methode mit Async beachten sollten, ist, dass sich innerhalb der Methode mindestens ein wartender Operator befindet. In Ihrem Beispiel würde ich es wie unten gezeigt mit TaskCompletionSource übersetzen .

private Task<int> DoWorkAsync()
{
    //create a task completion source
    //the type of the result value must be the same
    //as the type in the returning Task
    TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
    Task.Run(() =>
    {
        int result = 1 + 2;
        //set the result to TaskCompletionSource
        tcs.SetResult(result);
    });
    //return the Task
    return tcs.Task;
}

private async void DoWork()
{
    int result = await DoWorkAsync();
}
Ronald Ramos
quelle
26
Warum verwenden Sie TaskCompletionSource, anstatt nur die von der Task.Run () -Methode zurückgegebene Aufgabe zurückzugeben (und ihren Hauptteil so zu ändern, dass das Ergebnis zurückgegeben wird)?
ironisch
4
Nur eine Randnotiz. Eine Methode mit einer "async void" -Signatur ist im Allgemeinen eine schlechte Praxis und wird als schlechter Code angesehen, da dies ziemlich leicht zu einem Deadlock der Benutzeroberfläche führen kann. Die Hauptausnahme sind asynchrone Ereignishandler.
Jazzeroki
12

Wenn Sie Task.Run zum Ausführen einer Methode verwenden, ruft Task einen Thread aus dem Threadpool ab, um diese Methode auszuführen. Aus Sicht des UI-Threads ist es also "asynchron", da es den UI-Thread nicht blockiert. Dies ist für Desktop-Anwendungen in Ordnung, da Sie normalerweise nicht viele Threads benötigen, um sich um Benutzerinteraktionen zu kümmern.

Für Webanwendungen wird jedoch jede Anforderung von einem Thread-Pool-Thread bearbeitet, und daher kann die Anzahl der aktiven Anforderungen durch Speichern solcher Threads erhöht werden. Die häufige Verwendung von Threadpool-Threads zur Simulation eines asynchronen Vorgangs ist für Webanwendungen nicht skalierbar.

Bei True Async muss nicht unbedingt ein Thread für E / A-Vorgänge wie Datei- / DB-Zugriff usw. verwendet werden. Sie können dies lesen, um zu verstehen, warum für E / A-Vorgänge keine Threads erforderlich sind. http://blog.stephencleary.com/2013/11/there-is-no-thread.html

In Ihrem einfachen Beispiel handelt es sich um eine reine CPU-gebundene Berechnung, daher ist die Verwendung von Task.Run in Ordnung.

zheng yu
quelle
Wenn ich also eine synchrone externe API in einem Web-API-Controller verwenden muss, sollte ich den synchronen Aufruf NICHT in Task.Run () einschließen. Wie Sie sagten, wird dadurch der anfängliche Anforderungsthread nicht blockiert, es wird jedoch ein anderer Pool-Thread verwendet, um die externe API aufzurufen. Tatsächlich denke ich, dass es immer noch eine gute Idee ist, weil auf diese Weise theoretisch zwei Pool-Threads verwendet werden können, um viele Anforderungen zu verarbeiten, z. B. kann ein Thread viele eingehende Anforderungen verarbeiten und ein anderer kann die externe API für alle diese Anforderungen aufrufen?
stt106
Ich stimme nicht zu. Ich sage nicht, dass Sie nicht unbedingt alle synchronen Aufrufe in Task.Run () einschließen sollten. Ich weise nur auf mögliche Probleme hin.
Zheng yu
1
@ stt106 I should NOT wrap the synchronous call in Task.Run()das ist richtig. Wenn Sie dies tun, würden Sie nur die Threads wechseln. Das heißt, Sie entsperren den anfänglichen Anforderungsthread, nehmen jedoch einen anderen Thread aus dem Threadpool, der zur Verarbeitung einer anderen Anforderung hätte verwendet werden können. Das einzige Ergebnis ist ein Kontextwechsel-Overhead, wenn der Anruf für eine Verstärkung von absolut Null abgeschlossen ist
Saeb Amini,