Bewährte Verfahrensweisen für das Implementieren des ereignisbasierten asynchronen Entwurfsmusters

Das ereignisbasierte asynchrone Muster bietet Ihnen eine effektive Methode, um asynchrones Verhalten in Klassen mit einer vertrauten Ereignis- und Delegatsemantik verfügbar zu machen. Für eine Implementierung des ereignisbasierten asynchronen Musters müssen Sie einige spezielle Verhaltensanforderungen befolgen. In den folgenden Abschnitten sind Anforderungen und Richtlinien beschrieben, die Sie bei der Implementierung einer Klasse berücksichtigen sollten, die dem ereignisbasierten asynchronen Muster folgt.

Eine Übersicht finden Sie unter Implementieren des ereignisbasierten asynchronen Entwurfsmusters.

Erforderliche Verhaltensgarantien

Wenn Sie das ereignisbasierte asynchrone Muster implementieren, müssen Sie eine Reihe von Garantien bereitstellen, um sicherzustellen, dass sich Ihre Klasse ordnungsgemäß verhält und die Clients Ihrer Klasse sich auf dieses Verhalten verlassen können.

Completion

Rufen Sie immer den Ereignishandler MethodNameCompleted auf, wenn ein erfolgreicher Abschluss, ein Fehler oder ein Abbruch vorliegt. Anwendungen sollten niemals auf eine Situation treffen, in der sie im Leerlauf bleiben und ein Abschluss niemals erfolgt. Eine Ausnahme dieser Regel ist, wenn der asynchrone Vorgang an sich so entwickelt wurde, dass er nie abgeschlossen wird.

Abgeschlossenes Event und EventArgs

Wenden Sie für jede separate MethodNameAsync-Methode die folgenden Entwurfsanforderungen an:

  • Definieren Sie ein MethodNameCompleted-Ereignis in derselben Klasse wie die Methode.

  • Definieren Sie eine EventArgs-Klasse und einen begleitenden Delegaten für das MethodNameCompleted-Ereignis, das von der AsyncCompletedEventArgs-Klasse abgeleitet ist. Der Standardklassenname sollte die Form MethodNameCompletedEventArgs aufweisen.

  • Stellen Sie sicher, dass die Klasse EventArgs für die Rückgabewerte der Methode MethodName spezifisch ist. Wenn Sie die Klasse EventArgs verwenden, sollten Sie von Entwicklern niemals verlangen, das Ergebnis umzuwandeln.

    Das folgende Codebeispiel zeigt jeweils eine gute und eine schlechte Implementierung dieser Entwurfsanforderung.

// Good design
private void Form1_MethodNameCompleted(object sender, xxxCompletedEventArgs e)
{
    DemoType result = e.Result;
}

// Bad design
private void Form1_MethodNameCompleted(object sender, MethodNameCompletedEventArgs e)
{
    DemoType result = (DemoType)(e.Result);
}
  • Definieren Sie keine EventArgs-Klasse zum Zurückgeben von Methoden, die void zurückgeben. Verwenden Sie stattdessen eine Instanz der AsyncCompletedEventArgs-Klasse.

  • Stellen Sie sicher, dass Sie immer das MethodNameCompleted-Ereignis auslösen. Dieses Ereignis sollte bei einem erfolgreichen Abschluss, einem Fehler oder einem Abbruch ausgelöst werden. Anwendungen sollten niemals auf eine Situation treffen, in der sie im Leerlauf bleiben und ein Abschluss niemals erfolgt.

  • Stellen Sie sicher, dass Sie alle Ausnahmen abfangen, die im asynchronen Vorgang auftreten und die abgefangene Ausnahme der Eigenschaft Error zuweisen.

  • Wenn beim Abschließen der Aufgabe ein Fehler aufgetreten ist, sollte auf die Ergebnisse nicht zugegriffen werden können. Wenn die Eigenschaft Error nicht null entspricht, stellen Sie sicher, dass der Zugriff auf eine beliebige Eigenschaft in der EventArgs-Struktur eine Ausnahme auslöst. Verwenden Sie die Methode RaiseExceptionIfNecessary, um diese Überprüfung durchzuführen.

  • Modellieren Sie einen Timeout als Fehler. Wenn es zu einem Timeout kommt, lösen Sie das MethodNameCompleted-Ereignis aus und weisen der Eigenschaft Error eine TimeoutException zu.

  • Wenn Ihre Klasse mehrere gleichzeitige Aufrufe unterstützt, stellen Sie sicher, dass das MethodNameCompleted-Ereignis das geeignete userSuppliedState-Objekt enthält.

  • Stellen Sie sicher, dass das MethodNameCompleted-Ereignis im geeigneten Thread und zum geeigneten Zeitpunkt im Anwendungslebenszyklus ausgelöst wird. Weitere Informationen finden Sie im Abschnitt „Threading und Kontexte“.

Gleichzeitige Ausführung von Vorgängen

  • Wenn Ihre Klasse mehrere gleichzeitige Aufrufe unterstützt, ermöglichen Sie dem Entwickler, jeden Aufruf separat nachzuverfolgen. Hierzu definieren Sie die MethodNameAsync-Überladung, die einen State-Parameter mit Objektwert oder eine Task-ID namens userSuppliedState akzeptiert. Dieser Parameter sollte immer der letzte Parameter in der Signatur der MethodNameAsync-Methode sein.

  • Wenn Ihre Klasse die MethodNameAsync-Überladung definiert, die einen State-Parameter mit Objektwert oder eine Task-ID akzeptiert, stellen Sie sicher, dass Sie die Lebensdauer des Vorgangs mit dieser Task-ID nachverfolgen und eine Rückgabe an den Abschlusshandler erfolgt. Es sind Hilfsklassen zur Unterstützung verfügbar. Weitere Informationen zur Parallelitätsverwaltung finden Sie unter Gewusst wie: Implementieren einer Komponente, die das ereignisbasierte asynchrone Muster unterstützt.

  • Wenn Ihre Klasse die MethodNameAsync-Methode ohne den State-Parameter definiert und nicht mehrere gleichzeitige Aufrufe unterstützt, stellen Sie sicher, dass jeder Versuch, MethodNameAsync aufzurufen, bevor der vorherige MethodNameAsync-Aufruf abgeschlossen wurde, eine InvalidOperationException auslöst.

  • Im Allgemeinen sollte keine Ausnahme ausgelöst werden, wenn die MethodNameAsync-Methode ohne den userSuppliedState-Parameter mehrmals aufgerufen wird, sodass mehrere ausstehende Vorgänge vorhanden sind. Sie können eine Ausnahme auslösen, wenn Ihre Klasse diese Situation explizit nicht verarbeiten kann, aber gehen Sie davon aus, dass Entwickler diese mehrfachen, nicht unterscheidbare Rückrufe handhaben können.

Zugreifen auf Ergebnisse

Fortschrittsberichte

  • Unterstützen Sie die Statusberichterstellung, wenn möglich. Damit können Entwickler eine bessere Benutzerfreundlichkeit für die Anwendung bereitstellen, wenn sie Ihre Klasse verwenden.

  • Wenn Sie ein ProgressChanged- oder MethodNameProgressChanged-Ereignis implementieren, stellen Sie sicher, dass solche Ereignisse nicht für einen bestimmten asynchronen Vorgang ausgelöst werden, nachdem das MethodNameCompleted-Ereignis dieses Vorgangs ausgelöst wurde.

  • Wenn der Standardwert für ProgressChangedEventArgs ausgefüllt wird, stellen Sie sicher, dass ProgressPercentage immer als ein Prozentsatz interpretiert werden kann. Der Prozentsatz muss nicht präzise sein, sollte aber einen Prozentsatz darstellen. Wenn die Metrik für Ihre Statusberichterstellung etwas anderes als ein Prozentsatz sein muss, leiten Sie eine Klasse von der ProgressChangedEventArgs-Klasse ab, und behalten Sie 0 für ProgressPercentage bei. Vermeiden Sie die Verwendung einer anderen Berichtsmetrik als einen Prozentsatz.

  • Stellen Sie sicher, dass das ProgressChanged-Ereignis im geeigneten Thread und zum geeigneten Zeitpunkt im Anwendungslebenszyklus ausgelöst wird. Weitere Informationen finden Sie im Abschnitt „Threading und Kontexte“.

IsBusy-Implementierung

  • Machen Sie keine IsBusy-Eigenschaft verfügbar, wenn Ihre Klasse mehrere parallele Aufrufe unterstützt. XML-Webdienstproxies machen beispielsweise keine IsBusy-Eigenschaft verfügbar, weil sie mehrere parallele Aufrufe von asynchronen Methoden unterstützen.

  • Die IsBusy-Eigenschaft sollte true zurückgeben, nachdem die MethodNameAsync-Methode aufgerufen wurde und bevor das MethodNameCompleted-Ereignis ausgelöst wird. Andernfalls sollte false zurückgegeben werden. Die Komponenten BackgroundWorker und WebClient sind Beispiele für Klassen, die eine IsBusy-Eigenschaft verfügbar machen.

Abbruch

  • Unterstützen Sie einen Abbruch, wenn möglich. Damit können Entwickler eine bessere Benutzerfreundlichkeit für die Anwendung bereitstellen, wenn sie Ihre Klasse verwenden.

  • Im Fall eines Abbruchs legen Sie das Cancelled-Flag im AsyncCompletedEventArgs-Objekt fest.

  • Stellen Sie sicher, dass bei allen Versuchen, auf das Ergebnis zuzugreifen, eine InvalidOperationException ausgelöst wird, die angibt, dass der Vorgang abgebrochen wurde. Verwenden Sie die Methode AsyncCompletedEventArgs.RaiseExceptionIfNecessary, um diese Überprüfung durchzuführen.

  • Stellen Sie sicher, dass die Aufrufe an eine Abbruchmethode immer erfolgreich zurückgegeben werden und niemals eine Ausnahme auslösen. Im Allgemeinen wird ein Client nicht benachrichtigt, ob ein Vorgang zu einem gegebenen Zeitpunkt wirklich abbrechbar ist und ob ein zuvor ausgegebener Abbruch erfolgreich war. Die Anwendung wird jedoch immer benachrichtigt, wenn ein Abbruch erfolgreich war, da die Anwendung am Abschlussstatus teilnimmt.

  • Lösen Sie das MethodNameCompleted-Ereignis aus, wenn der Vorgang abgebrochen wird.

Fehler und Ausnahmen

  • Fangen Sie alle Ausnahmen ab, die im asynchronen Vorgang auftreten, und legen Sie den Wert der AsyncCompletedEventArgs.Error-Eigenschaft auf diese Ausnahme fest.

Threading und Kontexte

Für die korrekte Funktionsweise Ihrer Klasse ist es wichtig, dass die Ereignishandler des Clients im richtigen Thread oder Kontext des entsprechenden Anwendungsmodells aufgerufen werden, einschließlich ASP.NET- und Windows Forms-Anwendungen. Es werden zwei wichtige Hilfsklassen bereitgestellt, um sicherzustellen, dass sich Ihre asynchrone Klasse unter jedem Anwendungsmodell korrekt verhält: AsyncOperation und AsyncOperationManager.

AsyncOperationManager stellt eine Methode bereit, CreateOperation, die AsyncOperation zurückgibt. Ihre MethodNameAsync-Methode ruft CreateOperation auf, und Ihre Klasse verwendet den zurückgegebenen AsyncOperation, um die Lebensdauer des asynchronen Tasks nachzuverfolgen.

Für einen Statusbericht, inkrementelle Ergebnisse und Abschluss auf dem Client rufen Sie die Methoden Post und OperationCompleted in AsyncOperation auf. AsyncOperation ist für das Marshalling von Aufrufen an die Ereignishandler des Clients an den richtigen Thread oder Kontext verantwortlich.

Hinweis

Sie können diese Regeln umgehen, wenn Sie explizit gegen die Richtlinie des Anwendungsmodell vorgehen möchten, aber dennoch von den anderen Vorteilen der Verwendung des ereignisbasierten asynchronen Musters profitieren. Möglicherweise möchten Sie zum Beispiel, dass eine in Windows Forms betriebene Klasse eine Freethread-Klasse ist. Sie können eine Freethread-Klasse erstellen, solange Entwickler die implizierten Einschränkungen verstehen. Konsolenanwendungen führen keine Synchronisierung von Post-Aufrufen durch. Das kann dazu führen, dass ProgressChanged-Ereignisse nicht in der richtigen Reihenfolge ausgelöst werden. Wenn Sie eine serialisierte Ausführung von Post-Aufrufen wünschen, implementieren und installieren Sie eine System.Threading.SynchronizationContext-Klasse.

Weitere Informationen zur Verwendung von AsyncOperation und AsyncOperationManager für Ihre asynchronen Vorgänge finden Sie unter Gewusst wie: Implementieren einer Komponente, die das ereignisbasierte asynchrone Muster unterstützt.

Richtlinien

  • Idealerweise sollte jeder Methodenaufruf unabhängig von anderen erfolgen. Sie sollten vermeiden, Aufrufe mit freigegebenen Ressourcen zu koppeln. Wenn Ressourcen unter Aufrufen freigegeben werden sollen, müssen einen richtigen Synchronisierungsmechanismus in Ihrer Implementierung bereitstellen.

  • Entwürfe, bei denen der Client eine Synchronisierung implementieren muss, sind nicht empfehlenswert. Wenn Sie beispielsweise eine asynchrone Methode haben, die ein globales statisches Objekt als Parameter erhält, könnten mehrere parallele Aufrufe einer solchen Methode zur Datenbeschädigungen oder Deadlocks führen.

  • Wenn Sie eine Methode mit der Überladung mit mehreren Aufrufen (userState in der Signatur) implementieren, muss Ihre Klasse eine Sammlung von Benutzerstatus oder Aufgaben-IDs und ihre entsprechenden ausstehenden Vorgänge verwalten. Diese Auflistung sollte mit lock-Regionen geschützt werden, da die verschiedenen Aufrufe userState-Objekte in der Auflistung hinzufügen und daraus entfernen.

  • Ziehen Sie die erneute Verwendung von CompletedEventArgs-Klassen in Betracht, wenn diese machbar und angemessen ist. In diesem Fall ist die Benennung nicht konsistent mit dem Methodennamen, da ein gegebener Delegat und EventArgs-Typ nicht an eine einzige Methode gebunden sind. Dennoch ist es niemals akzeptabel, Entwickler zu zwingen, den von einer Eigenschaft in EventArgs abgerufenen Wert umzuwandeln.

  • Wenn Sie eine Klasse entwickeln, die von Component abgeleitet ist, implementieren und installieren Sie nicht Ihre eigene SynchronizationContext-Klasse. Die verwendete SynchronizationContext werden von Anwendungsmodellen, nicht von Komponenten gesteuert.

  • Wenn Sie Multithreading irgendeiner Art verwenden, setzen Sie sich potenziell ernsthaften und komplexen Fehlern aus. Bevor Sie eine Lösung implementieren, in der Multithreading verwendet wird, sollten Sie den Artikel Verwaltetes Threading – bewährte Methoden lesen.

Weitere Informationen