async / await - wann eine Aufgabe gegen void zurückgeben?

503

Unter welchen Szenarien möchte man verwenden

public async Task AsyncMethod(int num)

anstatt

public async void AsyncMethod(int num)

Das einzige Szenario, an das ich denken kann, ist, wenn Sie die Aufgabe benötigen, um ihren Fortschritt verfolgen zu können.

Sind bei der folgenden Methode außerdem die Schlüsselwörter asynchron und warten unnötig?

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}
user981225
quelle
20
Beachten Sie, dass asynchrone Methoden immer mit dem Namen Async.Example versehen Foo()werden sollten FooAsync().
Fred
30
@Fred Meistens, aber nicht immer. Dies ist nur die Konvention, und die akzeptierten Ausnahmen von dieser Konvention gelten für ereignisbasierte Klassen oder Schnittstellenverträge, siehe MSDN . Beispielsweise sollten Sie allgemeine Ereignishandler wie Button1_Click nicht umbenennen.
Ben
14
Nur eine Notiz, die Sie nicht Thread.Sleepfür Ihre Aufgaben verwenden sollten, die Sie await Task.Delay(num)stattdessen verwenden sollten
Bob Vale
45
@fred Ich bin damit nicht einverstanden. IMO, das ein asynchrones Suffix hinzufügt, sollte nur verwendet werden, wenn Sie eine Schnittstelle mit Synchronisierungs- und Asynchronisierungsoptionen bereitstellen. Schlumpf, der Dinge mit Async benennt, wenn es nur eine Absicht gibt, ist sinnlos. Ein typisches Beispiel Task.Delayist nicht Task.AsyncDelay, dass alle Methoden auf Aufgabe Async sind
Nicht geliebt
11
Ich hatte heute Morgen ein interessantes Problem mit einer Webapi 2-Controller-Methode, die async voidstattdessen als deklariert wurde async Task. Die Methode stürzte ab, weil ein als Mitglied des Controllers deklariertes Entity Framework-Kontextobjekt verwendet wurde, bevor die Ausführung der Methode abgeschlossen war. Das Framework hat den Controller entsorgt, bevor die Ausführung seiner Methode abgeschlossen ist. Ich habe die Methode in async Task geändert und es hat funktioniert.
Costa

Antworten:

417

1) Normalerweise möchten Sie a zurückgeben Task. Die Hauptausnahme sollte sein, wenn Sie einen Rückgabetyp benötigenvoid (für Ereignisse). Wenn es keinen Grund gibt, dem Anrufer awaitIhre Aufgabe zu verweigern , warum nicht?

2) asyncRückgabemethoden voidsind in einem anderen Aspekt etwas Besonderes: Sie stellen asynchrone Operationen der obersten Ebene dar und enthalten zusätzliche Regeln, die zum Tragen kommen, wenn Ihre Aufgabe eine Ausnahme zurückgibt. Der einfachste Weg, den Unterschied zu zeigen, ist anhand eines Beispiels:

static async void f()
{
    await h();
}

static async Task g()
{
    await h();
}

static async Task h()
{
    throw new NotImplementedException();
}

private void button1_Click(object sender, EventArgs e)
{
    f();
}

private void button2_Click(object sender, EventArgs e)
{
    g();
}

private void button3_Click(object sender, EventArgs e)
{
    GC.Collect();
}

fDie Ausnahme wird immer "beobachtet". Eine Ausnahme, die eine asynchrone Methode der obersten Ebene hinterlässt, wird einfach wie jede andere nicht behandelte Ausnahme behandelt. gDie Ausnahme wird nie beachtet. Wenn der Garbage Collector die Aufgabe bereinigt, sieht er, dass die Aufgabe zu einer Ausnahme geführt hat und niemand die Ausnahme behandelt hat. In diesem Fall wird der TaskScheduler.UnobservedTaskExceptionHandler ausgeführt. Sie sollten dies niemals zulassen. Um Ihr Beispiel zu verwenden,

public static async void AsyncMethod2(int num)
{
    await Task.Factory.StartNew(() => Thread.Sleep(num));
}

Ja, verwenden Sie asyncund awaithier stellen sie sicher, dass Ihre Methode immer noch korrekt funktioniert, wenn eine Ausnahme ausgelöst wird.

Weitere Informationen finden Sie unter: http://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Suizo
quelle
10
Ich meinte fstatt gin meinem Kommentar. Die Ausnahme von fwird an die übergeben SynchronizationContext. gwird ausgelöst UnobservedTaskException, UTEstürzt aber nicht mehr ab, wenn er nicht behandelt wird. Es gibt Situationen, in denen es akzeptabel ist, solche "asynchronen Ausnahmen" zu haben, die ignoriert werden.
Stephen Cleary
3
Wenn Sie WhenAnymit mehreren Tasks Ausnahmen haben. Sie müssen oft nur den ersten behandeln und möchten die anderen oft ignorieren.
Stephen Cleary
1
@StephenCleary Danke, ich denke, das ist ein gutes Beispiel, obwohl es von dem Grund abhängt, warum Sie zuerst anrufen, WhenAnyob es in Ordnung ist, die anderen Ausnahmen zu ignorieren: Der Hauptanwendungsfall, den ich dafür habe, wartet immer noch auf die verbleibenden Aufgaben, wenn sie abgeschlossen sind mit oder ohne Ausnahme.
10
Ich bin ein wenig verwirrt darüber, warum Sie empfehlen, eine Aufgabe zurückzugeben, anstatt sie zu stornieren. Wie Sie sagten, wird f () eine Ausnahme auslösen, g () jedoch nicht. Ist es nicht am besten, auf diese Ausnahmen im Hintergrund aufmerksam zu machen?
user981225
2
@ user981225 In der Tat, aber das liegt dann in der Verantwortung des Aufrufers von g: Jede Methode, die g aufruft, sollte asynchron sein und auch warten. Dies ist eine Richtlinie, keine harte Regel. Sie können entscheiden, dass es in Ihrem spezifischen Programm einfacher ist, g return void zu haben.
40

Ich bin auf diesen sehr nützlichen Artikel über Jérôme Laban gestoßen asyncund habe ihn voidgeschrieben: https://jaylee.org/archive/2012/07/08/c-sharp-async-tips-and-tricks-part-2-async-void .html

Die Quintessenz ist, dass ein async+voidSystem abstürzen kann und normalerweise nur auf den UI-seitigen Ereignishandlern verwendet werden sollte.

Der Grund dafür ist der vom AsyncVoidMethodBuilder verwendete Synchronisationskontext, der in diesem Beispiel keiner ist. Wenn kein Umgebungssynchronisationskontext vorhanden ist, wird jede Ausnahme, die vom Hauptteil einer asynchronen void-Methode nicht behandelt wird, im ThreadPool erneut ausgelöst. Während es anscheinend keinen anderen logischen Ort gibt, an dem diese Art von nicht behandelter Ausnahme ausgelöst werden könnte, besteht der unglückliche Effekt darin, dass der Prozess beendet wird, da nicht behandelte Ausnahmen auf dem ThreadPool den Prozess seit .NET 2.0 effektiv beenden. Sie können alle nicht behandelten Ausnahmen mit dem Ereignis AppDomain.UnhandledException abfangen, es gibt jedoch keine Möglichkeit, den Prozess von diesem Ereignis wiederherzustellen.

Beim Schreiben von UI-Ereignishandlern sind asynchrone void-Methoden irgendwie schmerzlos, da Ausnahmen genauso behandelt werden wie bei nicht asynchronen Methoden. Sie werden auf den Dispatcher geworfen. Es besteht die Möglichkeit, solche Ausnahmen zu beheben, was in den meisten Fällen mehr als richtig ist. Außerhalb von UI-Ereignishandlern sind asynchrone Void-Methoden jedoch irgendwie gefährlich zu verwenden und möglicherweise nicht so einfach zu finden.

Davide Icardi
quelle
Ist das korrekt? Ich dachte, Ausnahmen in asynchronen Methoden werden in Task erfasst. Und auch afaik gibt es immer einen Deafault SynchronizationContext
NM
30

Ich habe eine klare Vorstellung von diesen Aussagen.

  1. Asynchrone Void-Methoden haben unterschiedliche Semantik zur Fehlerbehandlung. Wenn eine Ausnahme aus einer asynchronen Task oder einer asynchronen Task-Methode ausgelöst wird, wird diese Ausnahme erfasst und im Task-Objekt platziert. Bei asynchronen Void-Methoden gibt es kein Task-Objekt. Daher werden alle Ausnahmen, die aus einer asynchronen Void-Methode ausgelöst werden, direkt im SynchronizationContext ausgelöst (SynchronizationContext repräsentiert einen Speicherort, an dem möglicherweise Code ausgeführt wird.), Der aktiv war, als die Async-Void-Methode aktiv war gestartet

Ausnahmen von einer Async Void-Methode können nicht mit Catch erfasst werden

private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception)
  {
    // The exception is never caught here!
    throw;
  }
}

Diese Ausnahmen können mit AppDomain.UnhandledException oder einem ähnlichen Sammelereignis für GUI / ASP.NET-Anwendungen beobachtet werden. Die Verwendung dieser Ereignisse für die reguläre Ausnahmebehandlung ist jedoch ein Rezept für die Nichtwartbarkeit (die Anwendung stürzt ab).

  1. Asynchrone Void-Methoden haben unterschiedliche Kompositionssemantiken. Asynchrone Methoden, die Task oder Task zurückgeben, können einfach mit await, Task.WhenAny, Task.WhenAll usw. erstellt werden. Asynchrone Methoden, die void zurückgeben, bieten keine einfache Möglichkeit, den abgeschlossenen Aufrufcode zu benachrichtigen. Es ist einfach, mehrere asynchrone Void-Methoden zu starten, aber es ist nicht einfach festzustellen, wann sie fertig sind. Async void-Methoden benachrichtigen ihren SynchronizationContext, wenn sie gestartet und beendet werden. Ein benutzerdefinierter SynchronizationContext ist jedoch eine komplexe Lösung für regulären Anwendungscode.

  2. Die asynchrone Void-Methode ist nützlich, wenn Sie den synchronen Ereignishandler verwenden, da sie ihre Ausnahmen direkt im SynchronizationContext auslösen. Dies ähnelt dem Verhalten synchroner Ereignishandler

Weitere Informationen finden Sie unter diesem Link: https://msdn.microsoft.com/en-us/magazine/jj991977.aspx

Nayas Subramanian
quelle
21

Das Problem beim Aufrufen von async void ist, dass Sie die Aufgabe nicht einmal zurückerhalten und nicht wissen können, wann die Aufgabe der Funktion abgeschlossen ist (siehe https://blogs.msdn.microsoft.com/oldnewthing/20170720-00/). p = 96655 )

Hier sind die drei Möglichkeiten, eine asynchrone Funktion aufzurufen:

async Task<T> SomethingAsync() { ... return t; }
async Task SomethingAsync() { ... }
async void SomethingAsync() { ... }

In allen Fällen wird die Funktion in eine Aufgabenkette umgewandelt. Der Unterschied besteht darin, was die Funktion zurückgibt.

Im ersten Fall gibt die Funktion eine Aufgabe zurück, die schließlich das t erzeugt.

Im zweiten Fall gibt die Funktion eine Aufgabe zurück, die kein Produkt enthält, auf die Sie jedoch warten können, um zu erfahren, wann sie vollständig ausgeführt wurde.

Der dritte Fall ist der böse. Der dritte Fall ist wie der zweite Fall, außer dass Sie die Aufgabe nicht einmal zurückbekommen. Sie können nicht wissen, wann die Aufgabe der Funktion abgeschlossen ist.

Der asynchrone Leerfall ist ein "Feuer und Vergessen": Sie starten die Task-Kette, aber es ist Ihnen egal, wann sie beendet ist. Wenn die Funktion zurückkehrt, wissen Sie nur, dass alles bis zum ersten Warten ausgeführt wurde. Alles nach dem ersten Warten wird an einem nicht festgelegten Punkt in der Zukunft ausgeführt, auf den Sie keinen Zugriff haben.

user8128167
quelle
6

Ich denke, Sie können damit auch async voidHintergrundoperationen starten, solange Sie darauf achten, Ausnahmen zu erwischen. Gedanken?

class Program {

    static bool isFinished = false;

    static void Main(string[] args) {

        // Kick off the background operation and don't care about when it completes
        BackgroundWork();

        Console.WriteLine("Press enter when you're ready to stop the background operation.");
        Console.ReadLine();
        isFinished = true;
    }

    // Using async void to kickoff a background operation that nobody wants to be notified about when it completes.
    static async void BackgroundWork() {
        // It's important to catch exceptions so we don't crash the appliation.
        try {
            // This operation will end after ten interations or when the app closes. Whichever happens first.
            for (var count = 1; count <= 10 && !isFinished; count++) {
                await Task.Delay(1000);
                Console.WriteLine($"{count} seconds of work elapsed.");
            }
            Console.WriteLine("Background operation came to an end.");
        } catch (Exception x) {
            Console.WriteLine("Caught exception:");
            Console.WriteLine(x.ToString());
        }
    }
}
bboyle1234
quelle
1
Das Problem beim Aufrufen von async void ist, dass Sie die Aufgabe nicht einmal zurückerhalten und nicht wissen können, wann die Aufgabe der Funktion abgeschlossen ist
user8128167
Dies ist sinnvoll für (seltene) Hintergrundoperationen, bei denen Sie sich nicht um das Ergebnis kümmern und keine Ausnahme andere Operationen beeinflussen soll. Ein typischer Anwendungsfall wäre das Senden eines Protokollereignisses an einen Protokollserver. Sie möchten, dass dies im Hintergrund geschieht, und Sie möchten nicht, dass Ihr Service- / Anforderungshandler fehlschlägt, wenn der Protokollserver ausfällt. Natürlich müssen Sie alle Ausnahmen abfangen, sonst wird Ihr Prozess beendet. Oder gibt es einen besseren Weg, dies zu erreichen?
Florian Winter
Ausnahmen von einer Async Void-Methode können nicht mit Catch erfasst werden. msdn.microsoft.com/en-us/magazine/jj991977.aspx
Si Zi
3
@SiZi Der Fang befindet sich INNERHALB der im Beispiel gezeigten asynchronen Void-Methode und wird abgefangen.
bboyle1234
Was ist, wenn sich Ihre Anforderungen dahingehend ändern, dass keine Arbeit erforderlich ist, wenn Sie sie nicht protokollieren können? Dann müssen Sie die Signaturen in Ihrer gesamten API ändern, anstatt nur die Logik zu ändern, die derzeit // Ausnahme ignorieren
Milney
0

Meine Antwort ist einfach: Sie können die Methode der Leere nicht abwarten

Error   CS4008  Cannot await 'void' TestAsync   e:\test\TestAsync\TestAsyncProgram.cs

Wenn die Methode also asynchron ist, ist es besser, auf sie zu warten, da Sie den asynchronen Vorteil verlieren können.

Serg Shevchenko
quelle
-2

Laut Microsoft-Dokumentation sollte NIEMALS verwendet werdenasync void

Tun Sie dies nicht: Im folgenden Beispiel wird verwendet async void, wodurch die HTTP-Anforderung abgeschlossen wird, wenn das erste Warten erreicht ist:

  • Was in ASP.NET Core-Apps IMMER eine schlechte Praxis ist.

  • Greift auf die HttpResponse zu, nachdem die HTTP-Anforderung abgeschlossen ist.

  • Absturz des Prozesses.

Robouste
quelle