Auf den Punkt gebracht: Async / Await Best Practices in .NET

Im Vorfeld des Kursbeginns erstellte "C # Developer" eine Übersetzung von interessantem Material.




Async / Await - Einführung


Das Async / Await-Sprachkonstrukt existiert seit C # Version 5.0 (2012) und wurde schnell zu einer der Säulen der modernen .NET-Programmierung. Jeder C # -Entwickler mit Selbstachtung sollte es verwenden, um die Anwendungsleistung, die allgemeine Reaktionsfähigkeit und die Lesbarkeit des Codes zu verbessern.

Async / Await macht die Einführung von asynchronem Code täuschend einfach und macht es für den Programmierer unnötig, die Details seiner Verarbeitung zu verstehen. Aber wie viele von uns wissen wirklich, wie es funktioniert und welche Vor- und Nachteile hat diese Methode? Es gibt viele nützliche Informationen, die jedoch fragmentiert sind. Deshalb habe ich beschlossen, diesen Artikel zu schreiben.

Dann beschäftigen wir uns mit dem Thema.

Zustandsmaschine (IAsyncStateMachine)


Das Erste, was Sie wissen müssen, ist, dass der Compiler jedes Mal, wenn Sie eine Methode oder Funktion mit Async / Await haben, Ihre Methode in eine generierte Klasse verwandelt, die die IAsyncStateMachine-Schnittstelle implementiert. Diese Klasse ist für die Aufrechterhaltung des Status Ihrer Methode während des Lebenszyklus einer asynchronen Operation verantwortlich. Sie kapselt alle Variablen Ihrer Methode in Form von Feldern und unterteilt Ihren Code in Abschnitte, die während Zustandsmaschinenübergängen zwischen Status ausgeführt werden, damit der Thread die Methode verlassen kann und wann wird zurückkehren, der Zustand wird sich nicht ändern.

Als Beispiel finden Sie hier eine sehr einfache Klassendefinition mit zwei asynchronen Methoden:

using System.Threading.Tasks;

using System.Diagnostics;

namespace AsyncAwait
{
    public class AsyncAwait
    {

        public async Task AsyncAwaitExample()
        {
            int myVariable = 0;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After First Await");
            myVariable = 1;

            await DummyAsyncMethod();
            Debug.WriteLine("Continuation - After Second Await");
            myVariable = 2;

        }

        public async Task DummyAsyncMethod()
        {
            // 
        }

    }
}

Eine Klasse mit zwei asynchronen Methoden

Wenn wir uns den Code ansehen, der während der Assembly generiert wurde, sehen wir ungefähr Folgendes:



Beachten Sie, dass zwei neue innere Klassen für uns generiert wurden, eine für jede asynchrone Methode. Diese Klassen enthalten eine Zustandsmaschine für jede unserer asynchronen Methoden.

Nachdem <AsyncAwaitExample> d__0wir den dekompilierten Code für untersucht haben , werden wir feststellen, dass unsere interne Variable «myVariable»jetzt ein Klassenfeld ist:



Wir können auch andere Klassenfelder sehen, die intern zur Aufrechterhaltung des Status verwendet werden IAsyncStateMachine. Die Zustandsmaschine durchläuft Zustände mit der MethodeMoveNext()in der Tat ein großer Schalter. Beachten Sie, wie die Methode nach jedem asynchronen Aufruf in verschiedenen Abschnitten fortgesetzt wird (mit der vorherigen Fortsetzungsbezeichnung).



Das heißt, asynchrone Eleganz hat ihren Preis. Die Verwendung von async / await erhöht die Komplexität (die Sie möglicherweise nicht kennen). In der serverseitigen Logik ist dies möglicherweise nicht kritisch. Insbesondere bei der Programmierung mobiler Anwendungen, die jeden CPU- und KB-Speicherzyklus berücksichtigen, sollten Sie dies berücksichtigen, da sich der Overhead schnell erhöhen kann. Später in diesem Artikel werden Best Practices für die Verwendung von Async / Await nur bei Bedarf erläutert.

Sehen Sie sich dieses Video auf YouTube an , um eine ziemlich lehrreiche Erklärung der Zustandsmaschine zu erhalten.

Wann wird Async / Await verwendet?


Es gibt im Allgemeinen zwei Szenarien, in denen Async / Await die richtige Lösung ist.

  • E / A-bezogene Arbeit : Ihr Code erwartet etwas, z. B. Daten aus einer Datenbank, Lesen einer Datei oder Aufrufen eines Webdienstes. In diesem Fall sollten Sie Async / Await verwenden, nicht die Task Parallel Library.
  • CPU-bezogene Arbeit : Ihr Code führt komplexe Berechnungen durch. In diesem Fall sollten Sie Async / Await verwenden, aber Sie müssen die Arbeit in einem anderen Thread mit Task.Run beginnen. Sie können auch die Task Parallel Library verwenden .



Async den ganzen Weg


Wenn Sie mit asynchronen Methoden arbeiten, werden Sie schnell feststellen, dass sich die asynchrone Natur des Codes in Ihrer Aufrufhierarchie auf und ab ausbreitet. Dies bedeutet, dass Sie Ihren aufrufenden Code auch asynchron machen müssen und so weiter.
Sie könnten versucht sein, dies zu stoppen, indem Sie den Code mit Task.Result oder Task.Wait blockieren, einen kleinen Teil der Anwendung konvertieren und in eine synchrone API einbinden, sodass der Rest der Anwendung von Änderungen isoliert ist. Leider ist dies ein Rezept für die Erstellung schwer zu verfolgender Deadlocks.

Die beste Lösung für dieses Problem besteht darin, asynchronen Code auf natürliche Weise in der Codebasis wachsen zu lassen. Wenn Sie dieser Entscheidung folgen, wird die Erweiterung des asynchronen Codes bis zu seinem Einstiegspunkt angezeigt, normalerweise eine Ereignishandler- oder Controller-Aktion. Geben Sie sich spurlos der Asynchronität hin!

Weitere Informationen finden Sie in diesem MSDN- Artikel .

Wenn die Methode als asynchron deklariert ist, stellen Sie sicher, dass darauf gewartet wird!


Wenn der Compiler eine asynchrone Methode findet, verwandelt er diese Methode in eine Zustandsmaschine. Wenn Ihr Code nicht in seinem Hauptteil wartet, generiert der Compiler eine Warnung, aber die Zustandsmaschine wird trotzdem erstellt, wodurch unnötiger Overhead für eine Operation entsteht, die niemals tatsächlich abgeschlossen wird.

Vermeiden Sie asynchrone Leere


Asynchrone Leere ist etwas, das wirklich vermieden werden sollte. Machen Sie es sich zur Regel, asynchrone Task anstelle von async void zu verwenden.

public async void AsyncVoidMethod()
{
    //!
}

public async Task AsyncTaskMethod()
{
    //!
}

Die Methoden async void und async Task Hierfür

gibt es mehrere Gründe, darunter:

  • Ausnahmen, die in der asynchronen Void-Methode ausgelöst werden, können außerhalb dieser Methode nicht abgefangen werden :
Wenn eine Ausnahme von der Methode " Async Task" oder "Async Task <T" ausgelöst >wird, wird diese Ausnahme abgefangen und in das Task-Objekt eingefügt. Bei Verwendung von asynchronen void-Methoden gibt es kein Task-Objekt. Daher werden alle Ausnahmen, die von der async void-Methode ausgelöst werden, direkt im SynchronizationContext aufgerufen, der aktiv war, als die async void-Methode ausgeführt wurde.

Betrachten Sie das folgende Beispiel. Der Erfassungsblock wird niemals erreicht.

public async void AsyncVoidMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public void ThisWillNotCatchTheException()
{
    try
    {
        AsyncVoidMethodThrowsException();
    }
    catch(Exception ex)
    {
        //     
        Debug.WriteLine(ex.Message);
    }
}

Ausnahmen, die in der async void-Methode ausgelöst werden, können außerhalb dieser Methode nicht abgefangen werden.

Vergleichen Sie mit diesem Code, bei dem anstelle von async void eine asynchrone Aufgabe verwendet wird. In diesem Fall ist der Fang erreichbar.

public async Task AsyncTaskMethodThrowsException()
{
    throw new Exception("Hmmm, something went wrong!");
}

public async Task ThisWillCatchTheException()
{
    try
    {
        await AsyncTaskMethodThrowsException();
    }
    catch (Exception ex)
    {
        //    
        Debug.WriteLine(ex.Message);
    }
}

Die Ausnahme wird abgefangen und im Task-Objekt platziert.

  • Asynchrone void-Methoden können unerwünschte Nebenwirkungen verursachen, wenn der Aufrufer nicht erwartet, dass sie asynchron sind : Wenn Ihre asynchrone Methode nichts zurückgibt, verwenden Sie asynchrone Task (ohne ein „ <T >“ für Task) als Rückgabetyp.
  • Async-Void-Methoden sind sehr schwer zu testen : Aufgrund der unterschiedlichen Fehlerbehandlung und des unterschiedlichen Layouts ist es schwierig, Komponententests zu schreiben, die Async-Void-Methoden aufrufen. Der asynchrone MSTest-Test funktioniert nur für asynchrone Methoden, die eine Aufgabe oder Aufgabe <T zurückgeben >.

Eine Ausnahme von dieser Vorgehensweise bilden asynchrone Ereignishandler. Aber auch in diesem Fall wird empfohlen, den im Handler selbst geschriebenen Code zu minimieren - erwarten Sie eine asynchrone Task-Methode, die Logik enthält.

Weitere Informationen finden Sie in diesem MSDN- Artikel .

Lieber Rückkehr Aufgabe statt Rückkehr warten


Wie bereits erläutert, erstellt der Compiler jedes Mal, wenn Sie eine Methode als asynchron deklarieren, eine Zustandsmaschinenklasse, die die Logik Ihrer Methode tatsächlich umschließt. Dies erhöht den Overhead, der sich insbesondere bei Mobilgeräten ansammeln kann, bei denen wir strengere Ressourcenbeschränkungen haben.

Manchmal muss eine Methode nicht asynchron sein, gibt jedoch Task <T zurück >und ermöglicht es der anderen Seite, sie entsprechend zu behandeln. Wenn der letzte Satz Ihres Codes eine erwartete Rückgabe ist, sollten Sie in Betracht ziehen, ihn umzugestalten, damit der Rückgabetyp der Methode Task <T ist>(anstelle von asynchronem T). Aus diesem Grund vermeiden Sie das Generieren einer Zustandsmaschine, wodurch Ihr Code flexibler wird. Der einzige Fall, auf den wir wirklich warten möchten, ist, wenn wir etwas mit der asynchronen Aufgabe tun, was zur Fortsetzung der Methode führt.

public async Task<string> AsyncTask()

{
   //  !
   //...  -  
   //await -   ,  await  

   return await GetData();

}

public Task<string> JustTask()

{
   //!
   //...  -  
   // Task

   return GetData();

}

Lieber Rückgabeaufgabe statt Rückgabe warten

Beachten Sie, dass die Rückgabe sofort erfolgt , wenn wir nicht warten und stattdessen Aufgabe <T zurückgeben. >Wenn sich der Code also in einem try / catch-Block befindet, wird die Ausnahme nicht abgefangen. Befindet sich der Code im using-Block, wird das Objekt sofort gelöscht. Siehe den nächsten Tipp.

Schließen Sie die Rückgabeaufgabe nicht in try..catch {} oder mit {} Blöcken ein


Rückgabeaufgabe kann undefiniertes Verhalten verursachen, wenn sie in einem try..catch-Block (eine von der asynchronen Methode ausgelöste Ausnahme wird niemals abgefangen) oder in einem using-Block verwendet wird, da die Aufgabe sofort zurückgegeben wird.

Wenn Sie Ihren asynchronen Code in einen try..catch- oder using-Block einschließen müssen, verwenden Sie stattdessen return await.

public Task<string> ReturnTaskExceptionNotCaught()

{
   try
   {
       // ...

       return GetData();

   }
   catch (Exception ex)

   {
       //     

       Debug.WriteLine(ex.Message);
       throw;
   }

}

public Task<string> ReturnTaskUsingProblem()

{
   using (var resource = GetResource())
   {

       // ...  ,     , ,    

       return GetData(resource);
   }
}

Wickeln Sie die Rückgabeaufgabe nicht in Blöcke try..catch{}oderusing{} .

Weitere Informationen in diesem Thread zum Stapelüberlauf.

Vermeiden Sie stattdessen die Verwendung von .Wait()oder.ResultGetAwaiter().GetResult()


Wenn Sie das Warten auf den Abschluss der Async-Aufgabe blockieren müssen, verwenden Sie alle Ausnahmen GetAwaiter().GetResult(). Waitund Resultwerfen Sie sie ein AggregateException, was die Fehlerbehandlung erschwert. Der Vorteil GetAwaiter().GetResult()ist, dass stattdessen die übliche Ausnahme zurückgegeben wird AggregateException.

public void GetAwaiterGetResultExample()

{
   // ,    ,     AggregateException  

   string data = GetData().Result;

   // ,   ,      

   data = GetData().GetAwaiter().GetResult();
}

Wenn Sie das Warten auf den Abschluss der Async-Aufgabe blockieren müssen, verwenden Sie die GetAwaiter().GetResult().

Informationen Weitere Informationen unter diesem Link .

Wenn die Methode asynchron ist, fügen Sie ihrem Namen das Async-Suffix hinzu


Dies ist die in .NET verwendete Konvention, um leichter zwischen synchronen und asynchronen Methoden zu unterscheiden (mit Ausnahme von Ereignishandlern oder Webcontroller-Methoden, die jedoch von Ihrem Code nicht explizit aufgerufen werden sollten).

Asynchrone Bibliotheksmethoden sollten Task.ConfigureAwait (false) verwenden, um die Leistung zu verbessern



Das .NET Framework hat das Konzept eines "Synchronisationskontexts", mit dem Sie "dorthin zurückkehren können, wo Sie zuvor waren". Immer wenn eine Aufgabe wartet, erfasst sie den aktuellen Synchronisationskontext, bevor sie wartet.

Nach Abschluss der Aufgabe wird .Post()die Synchronisationskontextmethode aufgerufen , mit der die Arbeit dort fortgesetzt wird, wo sie zuvor war. Dies ist nützlich, um zum Benutzeroberflächenthread zurückzukehren oder zum gleichen ASP.NET-Kontext usw. zurückzukehren.
Wenn Sie Bibliothekscode schreiben, müssen Sie selten zu dem Kontext zurückkehren, in dem Sie sich zuvor befanden. Wenn Task.ConfigureAwait (false) verwendet wird, versucht der Code nicht mehr, an der Stelle fortzufahren, an der er zuvor war. Stattdessen wird der Code nach Möglichkeit in dem Thread beendet, der die Aufgabe abgeschlossen hat, wodurch ein Kontextwechsel vermieden wird. Dies verbessert die Leistung geringfügig und kann dazu beitragen, Deadlocks zu vermeiden.

public async Task ConfigureAwaitExample()

{
   //   ConfigureAwait(false)   .

   var data = await GetData().ConfigureAwait(false);
}

Verwenden Sie normalerweise ConfigureAwait (false) für Serverprozesse und Bibliothekscode.
Dies ist besonders wichtig, wenn die Bibliotheksmethode für eine bessere Reaktionsfähigkeit häufig aufgerufen wird.

Verwenden Sie normalerweise ConfigureAwait (false) für Serverprozesse im Allgemeinen. Es ist uns egal, welcher Thread zum Fortfahren verwendet wird, im Gegensatz zu Anwendungen, in denen wir zum Thread der Benutzeroberfläche zurückkehren müssen.

Jetzt ... In ASP.NET Core hat Microsoft SynchronizationContext abgeschafft, sodass Sie das theoretisch nicht benötigen. Wenn Sie jedoch Bibliothekscode schreiben, der möglicherweise in anderen Anwendungen (z. B. UI-App, Legacy-ASP.NET, Xamarin Forms) wiederverwendet werden kann, bleibt dies eine bewährte Methode .

Sehen Sie sich dieses Video an , um eine gute Erklärung für dieses Konzept zu erhalten .

Asynchroner Aufgabenfortschrittsbericht


Ein ziemlich häufiger Anwendungsfall für asynchrone Methoden besteht darin, im Hintergrund zu arbeiten, den Benutzeroberflächenthread für andere Aufgaben freizugeben und die Reaktionsfähigkeit aufrechtzuerhalten. In diesem Szenario möchten Sie möglicherweise den Fortschritt an die Benutzeroberfläche zurückmelden, damit der Benutzer den Fortschritt des Prozesses überwachen und mit dem Vorgang interagieren kann.

Um dieses häufig auftretende Problem zu lösen, stellt .NET die IProgress <T- Schnittstelle bereit >, die die Report <T- Methode bereitstellt , die >von einer asynchronen Task aufgerufen wird, um dem Anrufer den Fortschritt zu melden. Diese Schnittstelle wird als Parameter der asynchronen Methode akzeptiert. Der Aufrufer muss ein Objekt bereitstellen, das diese Schnittstelle implementiert.

.NET bietet Progress <T >, die Standardimplementierung von IProgress <T >, die tatsächlich empfohlen wird, da es die gesamte Logik auf niedriger Ebene behandelt, die mit dem Speichern und Wiederherstellen des Synchronisationskontexts verbunden ist. Progress <T >bietet auch ein Action <T- Ereignis und einen Rückruf >- beide werden aufgerufen, wenn eine Aufgabe den Fortschritt meldet.

IProgress <T >und Progress <T >bieten zusammen eine einfache Möglichkeit, Fortschrittsinformationen von einer Hintergrundaufgabe an einen Benutzeroberflächenthread zu übertragen.

Bitte beachten Sie, dass <T.>Dies kann ein einfacher Wert sein, z. B. ein int, oder ein Objekt, das kontextbezogene Fortschrittsinformationen bereitstellt, z. B. den Fertigstellungsgrad, eine Zeichenfolgenbeschreibung der aktuellen Operation, ETA usw.
Überlegen Sie, wie oft Sie Fortschritte melden. Abhängig von der von Ihnen ausgeführten Operation stellen Sie möglicherweise fest, dass Ihre Codeberichte mehrmals pro Sekunde ausgeführt werden, was dazu führen kann, dass die Benutzeroberfläche weniger schnell reagiert. In einem solchen Szenario wird empfohlen, den Fortschritt in größeren Abständen zu melden.

Weitere Informationen finden Sie in diesem Artikel im offiziellen Microsoft .NET-Blog.

Asynchrone Aufgaben abbrechen


Ein weiterer häufiger Anwendungsfall für Hintergrundaufgaben ist die Möglichkeit, die Ausführung abzubrechen. .NET stellt die Klasse CancellationToken bereit. Die asynchrone Methode empfängt das CancellationToken-Objekt, das dann vom Code des Anrufers und der asynchronen Methode gemeinsam genutzt wird, wodurch ein Mechanismus zum Signalisieren der Löschung bereitgestellt wird.

Im häufigsten Fall erfolgt die Stornierung wie folgt:

  1. Der Aufrufer erstellt ein CancellationTokenSource-Objekt.
  2. Der Aufrufer ruft die abgebrochene asynchrone API auf und übergibt das CancellationToken von der CancellationTokenSource (CancellationTokenSource.Token).
  3. Der Aufrufer fordert eine Stornierung mit dem Objekt CancellationTokenSource (CancellationTokenSource.Cancel ()) an.
  4. Die Task bestätigt die Stornierung und bricht sich selbst ab, normalerweise mithilfe der CancellationToken.ThrowIfCancellationRequested-Methode.

Bitte beachten Sie, dass Sie, damit dieser Mechanismus funktioniert, Code schreiben müssen, um in regelmäßigen Abständen (d. H. Bei jeder Iteration Ihres Codes oder an einem natürlichen Haltepunkt in der Logik) nach Stornierungen zu suchen. Idealerweise sollte die asynchrone Aufgabe nach einer Abbruchanforderung so schnell wie möglich abgebrochen werden.

Sie sollten die Rückgängigmachung für alle Methoden in Betracht ziehen, deren Abschluss lange dauern kann.

Weitere Informationen finden Sie in diesem Artikel im offiziellen Microsoft .NET-Blog.

Fortschritts- und Stornierungsbericht - Beispiel


using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace TestAsyncAwait
{
   public partial class AsyncProgressCancelExampleForm : Form
   {
       public AsyncProgressCancelExampleForm()
       {
           InitializeComponent();
       }

       CancellationTokenSource _cts = new CancellationTokenSource();

       private async void btnRunAsync_Click(object sender, EventArgs e)

       {

           //   .

            <int>   ,          ,   ,    , ETA  . .

           var progressIndicator = new Progress<int>(ReportProgress);

           try

           {
               //   ,         

               await AsyncMethod(progressIndicator, _cts.Token);

           }

           catch (OperationCanceledException ex)

           {
               // 

               lblProgress.Text = "Cancelled";
           }
       }

       private void btnCancel_Click(object sender, EventArgs e)

       {
          // 
           _cts.Cancel();

       }

       private void ReportProgress(int value)

       {
           //    

           lblProgress.Text = value.ToString();

       }

       private async Task AsyncMethod(IProgress<int> progress, CancellationToken ct)

       {

           for (int i = 0; i < 100; i++)

           {
              //   ,     

               await Task.Delay(1000);

               //   

               if (ct != null)

               {

                   ct.ThrowIfCancellationRequested();

               }

               //   

               if (progress != null)

               {

                   progress.Report(i);
               }
           }
       }
   }
}

Warten auf eine gewisse Zeit


Wenn Sie eine Weile warten müssen (z. B. erneut versuchen, die Verfügbarkeit der Ressource zu überprüfen), müssen Sie Task.Delay verwenden. Verwenden Sie in diesem Szenario niemals Thread.Sleep.

Warten auf mehrere asynchrone Aufgaben


Verwenden Sie Task.WaitAny, um auf den Abschluss einer Aufgabe zu warten. Verwenden Sie Task.WaitAll, um auf den Abschluss aller Aufgaben zu warten.

Muss ich mich beeilen, um zu C # 7 oder 8 zu wechseln? Melden Sie sich für ein kostenloses Webinar an, um dieses Thema zu diskutieren.

All Articles