MSDN Magazin > Home > Ausgaben > 2008 > August >  Concurrent Affairs: Weitere AsyncEnumerator-Fea...
Concurrent Affairs
Weitere AsyncEnumerator-Features
Jeffrey Richter
Codedownload verfügbar unter: ConcurrentAffairs2008_08.exe (155 KB)
Code online durchsuchen
In meinem Artikel in der Rubrik „Concurrent Affairs“ vom November 2007 habe ich die Möglichkeit besprochen, C#-Sprachfeatures zu verwenden, um die asynchrone Programmierung zu vereinfachen (siehe msdn.microsoft.com/magazine/cc163323). Insbesondere habe ich mich auf anonyme Methoden, Lambda-Ausdrücke und Iteratoren konzentriert. Dann, in meinem Artikel vom Juni 2008, habe ich die AsyncEnumerator-Klasse eingeführt und erläutert, wie sie zur Steuerung eines C#-Iterators verwendet werden kann (siehe msdn.microsoft.com/magazine/cc546608). Ich habe auch die Architektur von AsyncEnumerator und ihre interne Funktionsweise erläutert.
In diesem Artikel möchte ich Ihnen einige zusätzliche Features zeigen, die AsyncEnumerator bietet, wie z. B. das Verknüpfen über mehrere gleichzeitige asynchrone Vorgänge hinweg, die Unterstützung für das asynchrone Programmiermodell (Asynchronous Programming Model, APM), Rückgabewerte, Rückruf-Threadsteuerung, Synchronisierung des Zugriffs auf freigegebene Daten, automatisches Verwerfen unvollendeter Vorgänge sowie Unterstützung für Abbruch/Timeout. Daneben werde ich Ihnen auch einige gängige Programmiermuster vorstellen, die mit AsyncEnumerator möglich werden.

Verknüpfen gleichzeitiger asynchroner Vorgänge
Einer der großen Vorzüge der Durchführung asynchroner Vorgänge besteht darin, dass mehrere gleichzeitig ausgeführt werden können, was die Leistung Ihrer Anwendung beträchtlich verbessert. Wenn Sie z. B. drei asynchrone Webdienstanforderungen gleichzeitig initiieren und wenn die Durchführung jeder Anforderung 5 Sekunden dauert, so beträgt die gesamte Wartezeit Ihres Programms nur 5 Sekunden. Wenn Sie dagegen synchrone Webdienstanforderungen durchführen, wartet die Anwendung, bis jede einzelne abgeschlossen ist, bevor sie die nächste initiiert. Das Durchführen dreier synchroner Webdienstanforderungen, von denen jede 5 Sekunden dauert, bedeutet also, dass Ihre Anwendung mindestens 15 Sekunden wartet.
Mit AsyncEnumerator ist es äußerst einfach, mehrere gleichzeitige asynchrone Vorgänge zu initiieren. Ihr Code kann dann die abgeschlossenen Vorgänge verarbeiten, nachdem alle abgeschlossen sind oder auch nach Abschluss jedes einzelnen. Der Iterator in Abbildung 1 zeigt beide Verfahren zum Verarbeiten abgeschlossener Vorgänge. Als ich ihn ausführte, erhielt ich die folgende Ausgabe (beachten Sie, dass Sie auf das Timeout der Webanforderung warten müssen):
All the operations completed:
   Uri=http://wintellect.com/        ContentLength=41207
   Uri=http://www.devscovery.com/    ContentLength=13258
   Uri=http://1.1.1.1/  WebException=Unable to connect to remote server
An operation completed:
   Uri=http://wintellect.com/        ContentLength=41208
An operation completed:
   Uri=http://www.devscovery.com/    ContentLength=13258
An operation completed:
   Uri=http://1.1.1.1/   WebException=Unable to connect to remote server
public static class AsyncEnumeratorPatterns {
  public static void Main() {
    String[] urls = new String[] { 
      "http://Wintellect.com/", 
      "http://1.1.1.1/",   // Demonstrates error recovery
      "http://www.Devscovery.com/" 
    };

    // Demonstrate process
    AsyncEnumerator ae = new AsyncEnumerator();
    ae.Execute(ProcessAllAndEachOps(ae, urls));
  }

  private static IEnumerator<Int32> ProcessAllAndEachOps(
       AsyncEnumerator ae, String[] urls) {
    Int32 numOps = urls.Length;

    // Issue all the asynchronous operation(s) so they run concurrently
    for (Int32 n = 0; n < numOps; n++) {
      WebRequest wr = WebRequest.Create(urls[n]);
      wr.BeginGetResponse(ae.End(), wr);
    }

    // Have AsyncEnumerator wait until ALL operations complete
    yield return numOps;

    Console.WriteLine("All the operations completed:");
    for (Int32 n = 0; n < numOps; n++) {
      ProcessCompletedWebRequest(ae.DequeueAsyncResult());
    }

    Console.WriteLine(); // *** Blank line between demos ***

    // Issue all the asynchronous operation(s) so they run concurrently
    for (Int32 n = 0; n < numOps; n++) {
      WebRequest wr = WebRequest.Create(urls[n]);
      wr.BeginGetResponse(ae.End(), wr);
    }

    for (Int32 n = 0; n < numOps; n++) {
      // Have AsyncEnumerator wait until EACH operation completes
      yield return 1;

      Console.WriteLine("An operation completed:");
      ProcessCompletedWebRequest(ae.DequeueAsyncResult());
    }
  }

  private static void ProcessCompletedWebRequest(IAsyncResult ar) {
    WebRequest wr = (WebRequest)ar.AsyncState;
    try {
      Console.Write("   Uri=" + wr.RequestUri + "    ");
      using (WebResponse response = wr.EndGetResponse(ar)) {
        Console.WriteLine("ContentLength=" + response.ContentLength);
      }
    }
    catch (WebException e) {
      Console.WriteLine("WebException=" + e.Message);
    }
    }
}
Der Code gibt am Anfang des Iterators mehrere asynchrone Vorgänge aus und führt dann eine yield return numOps-Anweisung aus. Diese Anweisung fordert AsyncEnumerator auf, keinen Rückruf in Ihren Code vorzunehmen, bis die Anzahl der Vorgänge, die durch den Wert der numOps-Variablen angegeben wird, abgeschlossen ist. Der Code direkt unter der yield return-Anweisung führt dann eine Schleife aus, um alle abgeschlossenen Vorgänge zu verarbeiten.
Beachten Sie, dass die Vorgänge in einer anderen Reihenfolge abgeschlossen werden können als jener, in der sie ausgegeben wurden. Um jedes Ergebnis mit seiner Anforderung zu korrelieren, übergab ich wr, das das WebRequest-Objekt identifiziert, das ich zur Initiierung der Anforderung verwende, im letzten Argument von BeginGetResponse. Dann, in der ProcessCompletedWebRequest-Methode, extrahiere ich das WebRequest-Objekt, das zur Initiierung der Anforderung verwendet wurde, aus der AsyncState-Eigenschaft von IAsyncResult.
Der Code am Ende des Iterators gibt ebenfalls mehrere asynchrone Vorgänge aus und tritt dann in eine Schleife ein, um jeden Vorgang nach Abschluss zu verarbeiten. Der Iterator muss jedoch zuerst warten, bis jeder Vorgang abgeschlossen ist. Dies wird durch die Anweisung „yield return 1“ erreicht, die AsyncEnumerator anweist, einen Rückruf in den Iteratorcode auszuführen, sobald ein einzelner Vorgang abgeschlossen ist.

APM-Unterstützung
In meinem letzten Artikel habe ich erklärt, wie das Aufrufen der Execute-Methode von AsyncEnumerator die Ausführung eines Iteratorcodes startet. Ich habe jedoch auch erklärt, dass der Thread, der Execute aufruft, blockiert ist, bis der Iterator beendet wird oder eine yield break-Anweisung ausführt.
Das Blockieren des Threads kann der Stabilität einer Anwendung schaden und sollte eigentlich vermieden werden, besonders in Serveranwendungen. Es beeinträchtigt auch die Reaktionsfähigkeit beim Aufrufen durch einen GUI-Thread, da der Iterator zur Ausführung eine unbestimmte Zeit in Anspruch nimmt, und während dieser Zeit reagiert eine Windows® Forms- oder Windows Presentation Foundation (WPF)-Anwendung nicht auf Eingaben. Execute sollte daher nur beim Schreiben von Testcode oder beim Experimentieren mit einer Iteratormethode aufgerufen werden.
Für Produktionscode sollten Sie die AsyncEnumerator-Methoden „BeginExecute“ und „EndExecute“ aufrufen. Wenn Sie BeginExecute aufrufen, konstruiert das AsyncEnumerator-Objekt intern eine Instanz der AsyncResultNoResult-Klasse, die ich in der Ausgabe des MSDN® Magazins von März 2007 (msdn.microsoft.com/magazine/cc163467) behandelt habe. Wenn Sie BeginExecute aufrufen, können Sie einen Verweis auf Ihre eigene AsyncCallback-Methode übergeben, und das AsyncEnumerator-Objekt ruft diese Methode dann auf, wenn die Ausführung des Iterators abgeschlossen ist. Diese Methode sollte dann die EndExececute-Methode von AsyncEnumerator aufrufen, um das Ergebnis des Iterators abzurufen. Ich werde später einige Beispiele zeigen, bei denen ich die Methoden „BeginExecute“ und „EndExecute“ nutze. So sehen die Methoden aus:
public class AsyncEnumerator<TResult>: AsyncEnumerator {
  public IAsyncResult BeginExecute(
    IEnumerator<Int32> enumerator,
    AsyncCallback callback, Object state);

  public void EndExecute(IAsyncResult result);
}
Da AsyncEnumerator das APM unterstützt, kann es in alle vorhandenen Microsoft® .NET Framework-Anwendungsmodelle integriert werden, da diese das APM bereits unterstützen. Dies bedeutet, dass Sie AsyncEnumerator mit ASP.NET Web Form-Anwendungen, ASP.NET XML-Webdiensten, WCF-Diensten (Windows Communication Foundation), Windows Forms-Anwendungen, WPF-Anwendungen, Konsolenanwendungen, Windows-Diensten usw. verwenden können.
Außerdem ist zu erwähnen, dass AsyncEnumerator, weil es das APM unterstützt, innerhalb eines anderen Iterators verwendet werden kann und die Komposition asynchroner Vorgänge ermöglicht. Sie könnten z. B. einen Iterator implementieren, der weiß, wie eine Datenbankanfrage asynchron gesendet und die Ergebnisse bei Eingang verarbeitet werden können. Ich nenne ihn den Subroutineniterator. Dann können Sie innerhalb eines anderen Iterators mehrere Datenbankanfragen initiieren, indem Sie den Subroutineniterator in einer Schleife aufrufen. Für jede Schleifeniteration konstruieren Sie einen AsyncEnumerator und rufen seine BeginExecute-Methode auf, wobei Sie den Namen des Subroutineniterators und die gewünschten zusätzlichen Argumente übergeben.
Beachten Sie, dass dieses Modell einen bedeutenden Vorteil bietet: Alle Subroutineniteratoren werden gleichzeitig ausgeführt, ohne irgendwelche Threads zu blockieren (es sei denn, die zugrunde liegende Implementierung des APM blockiert Threads, z. B. dann, wenn BeginXxx implementiert wurde, um einen Delegaten in die Warteschlange des Threadpools einzureihen, der blockiert, bis ein Vorgang abgeschlossen ist). Dies ermöglicht Ihnen, einen einfachen Iterator zu erstellen, der einen einzelnen asynchronen Vorgang einkapselt, und ihn aus anderen Iteratoren heraus aufzurufen und dabei Skalierbarkeit und Reaktionsfähigkeit zu erhalten.

Rückgabewerte
In vielen Szenarios ist es nützlich, wenn der Iterator ein Ergebnis zurückgibt, wenn er seine Verarbeitung ganz abgeschlossen hat. Ein Iterator kann jedoch nach Abschluss keinen Wert zurückgeben, da ein Iterator keine Rückgabeanweisung enthalten kann. Yield return-Anweisungen geben einen Wert für jede Iteration und keinen endgültigen Wert zurück.
Für den Fall, dass ein Iterator nach der Verarbeitung einen endgültigen Wert zurückgeben soll, habe ich die Hilfsklasse „AsyncEnumerator<TResult>“ erstellt. Das Modell für diese Klasse ist hier dargestellt:
public class AsyncEnumerator<TResult>: AsyncEnumerator {
  public AsyncEnumerator();
  public TResult Result { get; set; }

  new public TResult Execute(IEnumerator<Int32> enumerator);
  new public TResult EndExecute(IAsyncResult result);
}
Die Verwendung von AsyncEnumerator<TResult> ist einfach. Ändern Sie zuerst Ihren Code, um ein AsyncEnumerator<TResult> statt des normalen AsyncEnumerator zu instanziieren. Geben Sie für TResult den Typ an, den der Iterator letztendlich zurückgeben soll. Ändern Sie jetzt den Teil Ihres Codes, der die Methode „Execute“ oder „EndExecute“ aufruft (die vorher „void“ zurückgab), um den Rückgabewert zu erhalten, und verwenden Sie diesen Wert, wie Sie möchten.
Ändern Sie als Nächstes Ihren Iteratorcode, damit er ein AsyncEnumerator<TResult> statt eines AsyncEnumerator akzeptiert. Selbstverständlich müssen Sie den gleichen Datentyp für den generischen TResult-Parameter angeben. Abschließend setzen Sie innerhalb Ihres Iteratorcodes die Eigenschaft „Result“ des AsyncEnumerator<TResult>-Objekts auf den Wert, den der Iterator zurückgeben soll.
Um all dies zusammenzubringen, zeigt Abbildung 2 Code, der einen einfachen asynchronen ASP.NET-Webdienst implementiert, der gleichzeitig das HTML für mehrere verschiedene Websites anfordert (übergeben als durch Trennzeichen getrennte Zeichenfolge). Nachdem alle Websitedaten empfangen worden sind, gibt der Webdienst ein Array von Zeichenfolgen zurück, bei denen jedes Element die URL der Website und die Anzahl der Bytes zeigt, die von der betreffenden Website heruntergeladen wurden, oder auch einen Fehler, wenn ein solcher aufgetreten ist.
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
public class Service : System.Web.Services.WebService {
  private static List<String[]> s_recentRequests = new List<String[]>(10);
  private static SyncGate s_recentRequestsSyncGate = new SyncGate();

  private AsyncEnumerator<String[]> m_webSiteDataLength;

  [WebMethod]
  public IAsyncResult BeginGetWebSiteDataLength(
    String uris, AsyncCallback callback, Object state) {

    // Construct an AsyncEnumerator that will eventually return a String[]
    m_webSiteDataLength = new AsyncEnumerator<String[]>();

    // NOTE: The AsyncEnumerator automatically saves the ASP.NET 
    // SynchronizationContext with it ensuring that the iterator 
    // always executes using the correct IPrincipal, 
    // CurrentCulture, and CurrentUICulture.

    // Initiate the iterator asynchronously. 
    return m_webSiteDataLength.BeginExecute(
      GetWebSiteDataLength(m_webSiteDataLength, uris.Split(',')), 
                          callback, state);
    // NOTE: Since the AsyncEnumerator's BeginExecute method returns an 
    // IAsyncResult, we can just return this back to ASP.NET
  }

  private IEnumerator<Int32> GetWebSiteDataLength(
             AsyncEnumerator<String[]> ae, String[] uris) {

    // Issue several web request simultaneously
    foreach (String uri in uris) {
      WebRequest webRequest = WebRequest.Create(uri);
      webRequest.BeginGetResponse(ae.End(), webRequest);
    }

    yield return uris.Length;  // Wait for ALL the web requests to complete

    // Construct the String[] that will be the ultimate result
    ae.Result = new String[uris.Length];

    for (Int32 n = 0; n < uris.Length; n++) {
      // Grab the result of a completed web request
      IAsyncResult result = ae.DequeueAsyncResult();

      // Get the WebRequest object used to initate the request 
      WebRequest webRequest = (WebRequest)result.AsyncState;

      // Build the String showing the result of this completed web request
      ae.Result[n] = "URI=" + webRequest.RequestUri + ", ";

      using (WebResponse webResponse = webRequest.EndGetResponse(result)) {
        ae.Result[n] += "ContentLength=" + webResponse.ContentLength;
      }
    }

    // Modify the collection of most-recent queries
    s_recentRequestsSyncGate.BeginRegion(SyncGateMode.Exclusive, ae.End());
    yield return 1;   // Continue when collection can be updated (modified)

    // If collection is full, remove the oldest item
    if (s_recentRequests.Count == s_recentRequests.Capacity)
      s_recentRequests.RemoveAt(0);

    s_recentRequests.Add(ae.Result);
    s_recentRequestsSyncGate.EndRegion(ae.DequeueAsyncResult());   
  // Updating is done //
  }

  // ASP.NET calls this method when the iterator completes. 
  [WebMethod]
  public String[] EndGetWebSiteDataLength(IAsyncResult result) {
    return m_webSiteDataLength.EndExecute(result);
  }

  private AsyncEnumerator<String[][]> m_aeRecentRequests;

  [WebMethod]
  public IAsyncResult BeginGetRecentRequests(AsyncCallback callback, 
                                             Object state) {
    m_aeRecentRequests = new AsyncEnumerator<String[][]>();
    return m_aeRecentRequests.BeginExecute(GetRecentRequests(m
       _aeRecentRequests), callback, state);
  }

  private IEnumerator<Int32> GetRecentRequests(
     AsyncEnumerator<String[][]> ae) {
    // In a shared way, read the collection of most-recent requests
    s_recentRequestsSyncGate.BeginRegion(SyncGateMode.Shared, ae.End());
    yield return 1;   // Continue when collection can be examined (read)

    // Return a copy of the collection as an array
    ae.Result = s_recentRequests.ToArray();
    s_recentRequestsSyncGate.EndRegion(ae.DequeueAsyncResult());  
// Reading is done
  }

  [WebMethod]
  public String[][] EndGetRecentRequests(IAsyncResult result) {
    return m_aeRecentRequests.EndExecute(result);
  }
}

Rückruf-Threadsteuerung mit Synchronisierungskontexten
Wenn asynchrone Vorgänge abgeschlossen sind, werden verschiedene Threadpoolthreads aktiviert und benachrichtigen Ihr AsyncEnumerator-Objekt. Wenn AsyncEnumerator diese Threads verwendet hat, um einen Rückruf in Ihren Iterator auszuführen, kann der Code Ihres Iterators von verschiedenen Threads ausgeführt werden, obwohl im Code eines Iterators immer nur ein Thread ausgeführt werden kann. Für einige Szenarios ist es problematisch, wenn verschiedene Threads den Code des Iterators ausführen. So muss z. B. in einer Windows Forms- oder WPF Anwendung ein Steuerelement von dem Thread manipuliert werden, der das Steuerelement erstellt hat und der kein Threadpoolthread sein kann.
Wird der Iteratorcode über beliebige Threadpoolthreads ausgeführt, kann dies zu einem weiteren Problem führen. Bei einer ASP.NET-Anwendung beispielsweise geschieht Folgendes: Wenn eine Clientanforderung erstmals eingeht, ordnet ASP.NET den IPrincipal des Clients (für Identitätswechsel) sowie die Kulturinformation den Eigenschaften „CurrentPrincipal“, „CurrentCulture“ und „CurrentUICulture“ des Threadpoolthreads zu. Wenn Sie diesen Thread jedoch verwenden, um eine BeginXxx-Methode aufzurufen, dann sind, wenn der neue Threadpoolthread ausgeführt wird, um Sie vom Abschluss des Vorgangs zu benachrichtigen, bei neuen Threadpoolthreads diese Eigenschaften nicht automatisch richtig.
Um bei der Lösung dieser Probleme zu helfen, erlaubt die CLR, dass jedem Thread ein von SynchronizationContext abgeleitetes Objekt zugeordnet wird. Dieses Objekt kann dabei helfen, das von einem Anwendungsmodell verwendete Threadmodell zu erhalten. Bei Windows Forms und WPF wissen die von SynchronizationContext abgeleiteten Objekte, wie das Marshalling eines Funktionsaufrufs (der von einem Threadpoolthread initiiert wird) an den GUI-Thread erfolgen muss. Was ASP.NET anbetrifft, so weiß dessen von SynchronizationContext abgeleitetes Objekt, wie die Prinzipal- und Kultureigenschaften auf jeden Threadpoolthread initialisiert werden, der zur Verarbeitung einer einzelnen Anforderung verwendet wird.
Zur Gewährleistung des Threadmodells Ihrer Anwendung bietet AsyncEnumerator eine SyncContext-Eigenschaft. Diese Eigenschaft wird auf den Wert initialisiert, der von der statischen Current-Eigenschaft von SynchronizationContext innerhalb des Konstruktors von AsyncEnumerator zurückgegeben wird. Wenn dieser null ist, was bei einer Konsolen- oder Windows-Dienstanwendung die Regel ist, verwendet das Objekt immer, wenn ein Threadpoolthread das AsyncEnumerator-Objekt aufruft, diesen Thread, um einen Rückruf in Ihren Iterator auszuführen. Wenn jedoch die SyncContext-Eigenschaft nicht null ist, lässt das AsyncEnumerator-Objekt den Threadpoolthread Ihren Iterator über das angegebene, von SynchronizationContext abgeleitete Objekt aufrufen.
Für Windows Forms- und WPF-Anwendungen bedeutet das also, dass Ihr Iteratorcode immer über den GUI-Thread ausgeführt wird und Sie deshalb nur durch Ausführen von Code in Ihrem Iterator Steuerelemente im Formular aktualisieren können. Es gibt keinen Grund, die Methoden „Invoke“ und „BeginInvoke“ von Control oder Dispatcher aufzurufen. Dies macht es einfach, Ihren Iterator die Fortschrittsberichterstattung in Ihrer Benutzeroberfläche ausführen zu lassen, während asynchrone Vorgänge ausgeführt werden. Für ASP.NET bedeutet dies, dass die Prinzipal- und Kultureigenschaften immer richtig eingestellt werden, wenn der Iteratorcode ausgeführt wird. Sowohl im Code in Abbildung 2 als auch in dem Windows Forms-Beispiel, das ich Ihnen später zeigen werde, wird dieses Feature genutzt.

Synchronisieren des Zugriffs auf freigegebene Daten
In einigen Szenarios, besonders in serverseitigen Szenarios, haben Sie vielleicht mehrere AsyncEnumerator-Objekte (eines pro Clientanforderung), von denen jedes seine eigenen Iteratoren gleichzeitig verarbeitet. Stellen Sie sich z. B. eine Website vor, die auf eine Datenbank zugreift und dann einen Satz von Objekten im Speicher aktualisiert. Sie werden sicherlich das APM für den Datenbankzugriff verwenden wollen (z. B. durch Aufruf von BeginExecuteReader von SqlCommand), und dann müssen Sie vielleicht die speicherinternen Objekte auf eine threadsichere Weise aktualisieren. Normalerweise würden Sie wohl Methoden der Monitor-Klasse oder die C#-Anweisung „lock“ oder vielleicht das ReaderWriterLockSlim verwenden, das mit .NET Framework 3.5 geliefert wird. Alle dieser Sperren können jedoch den aufrufenden Thread blockieren, was die Skalierbarkeit und Reaktionsfähigkeit beeinträchtigt. Um das Blockieren von Threads zu vermeiden, neige ich dazu, mein ReaderWriterGate zu verwenden, das ich in meinem Artikel von Mai 2006 beschrieben habe (siehe msdn.microsoft.com/magazine/cc163532).
Als ich begann, ReaderWriterGate mit AsyncEnumerator zu verwenden, habe ich gemerkt, dass das Objektmodell von ReaderWriterGate verbessert werden könnte, um die Integration in AsyncEnumerator zu verbessern. So habe ich eine neue Klasse namens „SyncGate“ erstellt, die sich sehr ähnlich verhält wie ReaderWriterGate. Hier ist ihr Modell:
   public sealed class SyncGate {
     public SyncGate();
     public void BeginRegion(SyncGateMode mode, 
       AsyncCallback asyncCallback); 
     public void EndRegion(IAsyncResult ar); 
   }
   public enum SyncGateMode { Exclusive, Shared }
Wenn Sie mehrere Iteratoren haben, die ausgeführt werden und auf freigegebene Daten zugreifen wollen, erstellen Sie zuerst ein SyncGate, und speichern Sie dann einen Verweis darauf in einem statischen Feld, oder übermitteln Sie den Verweis darauf auf irgendeine Weise an die verschiedenen Iteratoren. Rufen Sie dann innerhalb der Iteratoren, kurz vor dem Code, der die freigegebenen Daten verwendet, die BeginRegion-Methode von SyncGate auf, und geben Sie an, ob der Code exklusiven Zugriff (Schreibzugriff) oder gemeinsamen Zugriff (Lesezugriff) auf die Daten benötigt. Veranlassen Sie den Iterator dann zu einem „yield return 1“. An dieser Stelle gibt Ihr Iterator seinen Thread auf, und wenn Ihr Iterator sicher auf die Daten zugreifen kann, führt AsyncEnumerator wieder automatisch einen Rückruf in Ihren Code aus. Dies bedeutet, dass keine Threads blockiert sind, während auf Zugriff auf die freigegebenen Daten gewartet wird.
Rufen Sie in Ihrem Iterator kurz nach dem Code, der die freigegebenen Daten verwendet, EndRegion auf. Dadurch wird SyncGate mitgeteilt, dass Ihr Code mit den freigegebenen Daten fertig ist, und anderen Iteratoren wird ermöglicht, nach Bedarf darauf zuzugreifen. In Abbildung 2 verwendet der GetWebSiteDataLength-Iterator am Ende SyncGate für den exklusiven Zugriff auf eine statische Sammlung. In Abbildung 2 zeigt der GetRecentRequests-Iterator, wie gemeinsamer Zugriff auf die gleiche Sammlung ermöglicht wird.

Verwerfungsgruppen
Ein anderes Feature, das von der AsyncEnumerator-Klasse angeboten wird, sind Verwerfungsgruppen. Dieses Feature ermöglicht Ihrem Iterator, mehrere gleichzeitige asynchrone Vorgänge auszugeben und dann später zu entscheiden, dass er an einigen davon (oder allen) nicht interessiert ist, sodass das AsyncEnumerator-Objekt die Ergebnisse beim Abschluss der verbleibenden Vorgänge automatisch verwirft.
Stellen Sie sich zum Beispiel Code vor, der die Temperatur in einer Stadt abrufen will. Es gibt viele Webdienste, die Sie abfragen könnten, um diese Informationen zu erhalten. Sie könnten einen Iterator schreiben, der drei Webdienste abfragt, um diese Informationen zu beschaffen. Sobald einer von ihnen die Temperatur zurückgibt, können Sie die Ergebnisse der anderen zwei Webdienste verwerfen. Einige Entwickler verwenden dieses Muster, um die Leistung ihrer Anwendungen zu verbessern.
Ein weiteres Beispiel ist, wenn ein Iterator eine Folge von Vorgängen durchführt und der Benutzer sie abbrechen möchte, weil er nicht mehr warten will oder sich anders entschieden hat. Ein ähnliches Szenario liegt vor, wenn Sie einen asynchronen Vorgang starten, Sie jedoch, wenn er in einem festgelegten Zeitraum nicht abgeschlossen ist, alle noch nicht abgeschlossenen Vorgänge verwerfen möchten. Einige Webdienste verarbeiten z. B. die Anfrage eines Clients, und wenn die ganze Verarbeitung nicht in z. B. zwei Sekunden abgeschlossen werden kann, teilt der Dienst lieber dem Client mit, dass die Anfrage fehlgeschlagen ist, statt den Client für unbestimmte Zeit auf die Antwort warten zu lassen.
So werden Verwerfungsgruppen verwendet: Innerhalb Ihres Iterators gruppieren Sie eine Reihe verwandter Vorgänge als Teil einer Verwerfungsgruppe. Eine Verwerfungsgruppe ist einfach nur ein Int32-Wert im Bereich von 0 bis 63. Sie können z. B. ein Bündel von BeginXxx-Methoden ausgeben, die anzeigen, dass sie alle Teil der Verwerfungsgruppe 0 sind. Dann kann Ihr Iterator einige davon verarbeiten, sobald sie abgeschlossen sind. Wenn Sie in Ihrem Iteratorcode entscheiden, dass Sie keine weiteren Vorgänge, die Teil der Verwerfungsgruppe sind, mehr verarbeiten möchten, rufen Sie die DiscardGroup-Methode von AsyncEnumerator auf:
public void DiscardGroup(Int32 discardGroup);
Diese Methode weist das AsyncEnumerator-Objekt an, alle verbleibenden Vorgänge zu verwerfen, die als Teil der angegebenen Verwerfungsgruppe ausgegeben wurden, sodass Ihr Iteratorcode nie irgendwelche dieser Vorgänge sieht, wenn er DequeueAsyncResult aufruft. Leider reicht dies nicht wirklich aus, da das .NET-APM erfordert, dass für jede BeginXxx-Methode EndXxx-Methoden aufgerufen werden, damit keine Ressourcenverluste auftreten.
Um diesen Anforderungen entgegenzukommen, muss AsyncEnumerator die richtige EndXxx-Methode für Vorgänge, die er verwirft, aufrufen. Da es für AsyncEnumerator keine Möglichkeit gibt, selbst herauszufinden, welche EndXxx-Methode aufzurufen ist, müssen Sie ihm dies mitteilen. Wenn Sie eine BeginXxx-Methode aufrufen, müssen Sie, statt einfach ae.End für das AsyncCallback-Argument zu übergeben, eine der folgenden Methoden übergeben:
AsyncCallback End(Int32 discardGroup, EndObjectXxx callback);
AsyncCallback EndVoid(Int32 discardGroup, EndVoidXxx callback);
EndObjectXxx und EndVoidXxx sind wie folgt definierte Delegaten:
delegate Object EndObjectXxx(IAsyncResult result);
delegate void EndVoidXxx(IAsyncResult result);
Wenn eine BeginXxx-Methode eine zugehörige EndXxx-Methode hat, die einen Wert zurückgibt, würden Sie die soeben gezeigte End-Methode aufrufen. Wenn Sie eine BeginXxx-Methode aufrufen, die eine entsprechende EndXxx-Methode hat, die „void“ zurückgibt (der ungewöhnliche Fall), würden Sie die EndVoid-Methode aufrufen. Wenn Sie jetzt AsyncEnumerator anweisen, eine Gruppe zu verwerfen, weiß er, welche EndXxx-Methode aufgerufen werden muss.
Beachten Sie, dass, wenn die EndXxx-Methode irgendeine Ausnahme auslöst, AsyncEnumerator diese auffängt und ignoriert. Das liegt daran, dass Sie, wenn Sie den Vorgang verwerfen, anzeigen, dass es Ihnen egal ist, ob der Vorgang erfolgreich abgeschlossen wurde oder fehlgeschlagen ist.
Ich sollte auch darauf hinweisen, dass, wenn Ihr Iterator die Ausführung beendet oder eine yield break-Anweisung ausführt, AsyncEnumerator automatisch alle Verwerfungsgruppen verwirft – wenn Sie den Iterator beenden, zeigen Sie an, dass Sie an den noch nicht verarbeiteten Vorgängen nicht interessiert sind. Dies kann sehr günstig sein, weil es Ihrem Iterator ermöglicht, einige asynchrone Vorgänge auszugeben, so viele abgeschlossene Vorgänge wie benötigt zu verarbeiten und sich dann einfach zu beenden.
AsyncEnumerator räumt automatisch verbleibende Vorgänge auf, deren Abschluss in der Zukunft liegt. Beachten Sie jedoch, dass die Vorgänge durch das Verwerfen nicht abgebrochen werden. Wenn einer der ausstehenden asynchronen Vorgänge in eine Datei schrieb oder eine Datenbank aktualisierte, verhindert das Verwerfen der relevanten Gruppen nicht, dass diese Vorgänge abgeschlossen werden. Es erlaubt Ihrem Code einfach nur, ungeachtet des Abschlusses der Vorgänge fortzufahren.

Abbruch
AsyncEnumerator macht es möglich, dass externer Code den Iterator während seiner Verarbeitung abbricht. Dieses Feature ist besonders nützlich für Windows Forms- und WPF-Anwendungen, da es ungeduldigen Benutzern ermöglicht, einen laufenden Vorgang abzubrechen und wieder die Kontrolle über die Anwendung zu übernehmen. AsyncEnumerator ist auch in der Lage, sich nach einem festgelegten Zeitraum selbst abzubrechen. Dieses Feature ist nützlich für Serveranwendungen, die die Zeit beschränken möchten, die es dauern darf, bis auf die Anfrage eines Clients geantwortet wird. Die mit dem Abbruch in Zusammenhang stehenden Methoden sind hier gezeigt:
public class AsyncEnumerator {
  // Call this method from inside the iterator
  public Boolean IsCanceled(out Object cancelValue);

  // Call this method from inside the iterator
  public Boolean IsCanceled();

  // Call this method from code outside the iterator
  public Boolean Cancel(Object cancelValue};

  // Call this method from code inside or outside the iterator
  public void SetCancelTimeout(Int32 milliseconds,
    Object cancelValue);
}
Um die Abbruchmöglichkeit innerhalb Ihres Iterators zu nutzen, geben Sie jeden asynchronen Vorgang als Teil einer Verwerfungsgruppe aus. Dies ermöglicht dem AsyncEnumerator automatisch, Vorgänge zu verwerfen, die nach der Abbruchanforderung abgeschlossen werden. Dann bauen Sie, um eine Abbruchanforderung zu erkennen, nach jeder yield return-Anweisung Code wie den unten in Abbildung 3 dargestellten ein.
IEnumerator<Int32> MyIterator(AsyncEnumerator ae, ...) {
  // obj refers to some object that has BeginXxx/EndXxx methods
  obj.BeginXxx(...,         // Pass any arguments as usual
    ae.End(0, obj.EndXxx),  // For AsyncCallback indicate
                            // discard group 0 & proper End method  
                            //to call for cleanup
    null);                  // BeginXxx's AsyncState argument

  // Make more calls to BeginXxx methods if desired here...

  yield return n; // Resume iterator after 'n' operations
                  // complete or if cancelation occurs 

  // Check for cancellation
  Object cancelValue;
  if (ae.IsCanceled(out cancelValue)) {
    // The iterator should cancel due to user request/timeout
    // Note: It is common to call "yield break" here.
  } else {
    // Call DequeueAsyncResult 'n' times to 
    // process the completed operations
  }
}
Wenn Sie jetzt mit der Ausführung eines abbrechbaren Iterators beginnen möchten, konstruieren Sie ein AsyncEnumerator-Objekt und rufen wie gewohnt BeginExecute auf. Wenn dann ein Teil Ihrer Anwendung den Iterator abbrechen will, wird die Cancel-Methode aufgerufen. Beim Aufrufen von Cancel können Sie der Methode einen Verweis auf ein Objekt übergeben, der dann über den out-Parameter der IsCanceled-Methode an den Iterator übergeben wird. Dieses Objekt gibt dem Code, der den Iterator abbricht, eine Möglichkeit, dem Iterator zu übermitteln, warum er abgebrochen wird. Wenn der Iterator nicht wissen möchte, warum er abgebrochen wird, kann er die überladene IsCanceled-Methode aufrufen, die keine Parameter verwendet.
Die SetCancelTimeout-Methode kann von Code innerhalb und außerhalb des Iterators aufgerufen werden. Wenn diese Methode aufgerufen wird, richtet sie einen Zeitgeber ein, der Cancel automatisch aufruft (und den Wert übergibt, den Sie über das CancelValue-Argument angeben), wenn die Zeit abgelaufen ist.
Der Code in Abbildung 4 zeigt eine Windows Forms-Anwendung, die viele der Features verwendet, die in diesem Artikel diskutiert werden. Die Benutzeroberfläche für die Anwendung wird in Abbildung 5 gezeigt. Sie verwendet das SyncContext-Feature von AsyncEnumerator, um sicherzustellen, dass der gesamte Iteratorcode über den GUI-Thread ausgeführt wird, sodass die Benutzeroberflächen-Steuerelemente aktualisiert werden können. Dieser Code zeigt auch, wie die APM-Unterstützung von AsyncEnumerator verwendet wird, um den GUI-Thread nicht zu blockieren, damit die Benutzeroberfläche weiterhin reaktionsfähig bleibt.
namespace WinFormUsingAsyncEnumerator {
  public partial class WindowsFormsViaAsyncEnumerator : Form {
    public static void Main() {
      Application.Run(new WindowsFormsViaAsyncEnumerator());
    }

    public WindowsFormsViaAsyncEnumerator() {
      InitializeComponent();
    }

    private AsyncEnumerator m_ae = null;

    private void m_btnStart_Click(object sender, EventArgs e) {
      String[] uris = new String[] {
        "http://Wintellect.com/", 
        "http://1.1.1.1/",   // Demonstrates error recovery
        "http://www.Devscovery.com/" 
      };

      m_ae = new AsyncEnumerator();

      // NOTE: The AsyncEnumerator automatically saves the 
      // Windows Forms SynchronizationContext with it ensuring
      // that the iterator always runs on the GUI thread; 
      // this allows the iterator to access the UI Controls

      // Start iterator asynchonously so that GUI thread doesn't block
      m_ae.BeginExecute(GetWebData(m_ae, uris), m_ae.EndExecute);
    }

    private IEnumerator<Int32> GetWebData(AsyncEnumerator ae, String[] uris) {
      ToggleStartAndCancelButtonState(false);
      m_lbResults.Items.Clear();

      if (m_chkAutoCancel.Checked)
        ae.SetCancelTimeout(5000, ae);

      // Issue several Web requests (all in discard group 0) simultaneously
      foreach (String uri in uris) {
        WebRequest webRequest = WebRequest.Create(uri);

        // If the AsyncEnumerator is canceled, DiscardWebRequest cleans up
        // any outstanding operations as they complete in the future
        webRequest.BeginGetResponse(ae.EndVoid(0, DiscardWebRequest), 
                                                          webRequest);
      }

      yield return uris.Length;  // Process the completed Web requests 
                                 // after all complete

      String resultStatus; // Ultimate result of processing shown to user

      // Check if iterator was canceled
      Object cancelValue;
      if (ae.IsCanceled(out cancelValue)) {
        ae.DiscardGroup(0);
        // Note: In this example calling DiscardGroup above is not mandatory
        // because the whole iterator is stopping execution; causing all
        // discard groups to be discarded automatically.

        resultStatus = (cancelValue == ae) ? "Timeout" : "User canceled";
        goto Complete;
      }

      // Iterator wasn't canceled, process all the completed operations
      for (Int32 n = 0; n < uris.Length; n++) {
        IAsyncResult result = ae.DequeueAsyncResult();

        WebRequest webRequest = (WebRequest)result.AsyncState;

        String s = "URI=" + webRequest.RequestUri + ", ";
        try {
          using (WebResponse webResponse = webRequest. 
                 EndGetResponse(result)) {
                    s += "ContentLength=" + webResponse.ContentLength;
          }
        }
        catch (WebException e) {
          s += "Error=" + e.Message;
        }
        m_lbResults.Items.Add(s);  // Add result of operation to listbox
      }
      resultStatus = "All operations completed.";

    Complete:
      // All operations have completed or cancellation occurred, tell      // user
      MessageBox.Show(this, resultStatus);

      // Reset everything so that the user can start over if they desire
      m_ae = null;   // Reset since we're finished
      ToggleStartAndCancelButtonState(true);
    }

    private void m_btnCancel_Click(object sender, EventArgs e) {
      m_ae.Cancel(null);
      m_ae = null;
    }

    // Swap the Start/Cancel button states
    private void ToggleStartAndCancelButtonState(Boolean enableStart) {
      m_btnStart.Enabled = enableStart;
      m_btnCancel.Enabled = !enableStart;
    }

    private void DiscardWebRequest(IAsyncResult result) {
      // Get the WebRequest object used to initate the request 
      // (see BeginGetResponse's last argument)
      WebRequest webRequest = (WebRequest)result.AsyncState;

      // Clean up the async operation and Close the WebResponse (if no       // exception)
      webRequest.EndGetResponse(result).Close();
    }
  }
}
Abbildung 5 Die Beispielanwendung
Innerhalb des Iterators werden viele Webanforderungen als Teil einer Verwerfungsgruppe ausgegeben, und weil die Benutzeroberfläche reaktionsfähig bleibt, können Benutzer auf die Abbrechen-Schaltfläche klicken, wenn es ihnen zu lange dauert. Wenn dies geschieht, schließt AsyncEnumerator automatisch die Vorgänge ab, damit Ihr Iteratorcode sich nicht um das Aufräumen kümmern muss. Beachten Sie, dass das Formular auch vorführt, wie ein Zeitgeber eingerichtet wird, sodass sich AsyncEnumerator automatisch nach fünf Sekunden abbricht, wenn nicht alle Vorgänge abgeschlossen sind.
Dieses Beispiel sendet Webanforderungen unter Verwendung der .NET-WebRequest-Klasse. Beim Aufrufen der BeginGetResponse-Methode von WebRequest erfordert die Bereinigung mehr als den Aufruf von EndGetResponse. Sie müssen auch Close (oder Dispose) für das WebResponse-Objekt aufrufen, das von EndGetResponse zurückgegeben wird.
Aus diesem Grund übergibt der Code beim Aufrufen von BeginGetResponse die DiscardWebRequest-Methode an die EndVoid-Methode. Die DiscardWebRequest-Methode stellt sicher, dass das WebResponse-Objekt geschlossen wird, wenn die Webanforderung erfolgreich war, und keine Ausnahme ausgelöst wird.

Viele Entwickler wissen, dass die asynchrone Programmierung der Schlüssel zu Steigerung der Leistung, Skalierbarkeit, Reaktionsfähigkeit und Zuverlässigkeit ihrer Anwendungen, Server und Komponenten ist. Leider lehnen viele Entwickler es ab, die asynchrone Programmierung vollständig zu übernehmen, weil das Programmiermodell bisher so viel mühsamer und schwieriger war als das erprobte und bewährte synchrone Programmiermodell.
Die Verwendung des C#-Iteratorfeatures und meiner AsyncEnumerator-Klasse erlaubt es Entwicklern, die asynchrone Programmierung aus einem synchronen Programmiermodell heraus zu übernehmen. AsyncEnumerator lässt sich auch problemlos in andere Teile von .NET Framework integrieren und bietet viele Features, die es Entwicklern ermöglichen, ihre Anwendungen über das hinaus zu erweitern, was mit dem üblichen synchronen Programmiermodell möglich ist.
Ich verwende AsyncEnumerator seit mehr als einem Jahr und habe vielen Unternehmen geholfen, es mit hervorragenden Ergebnissen in ihre Software zu integrieren. Laden Sie den Code unter wintellect.com/PowerThreading.aspx herunter. Ich hoffe, dass Sie dadurch genauso viel gewinnen werden wie ich.

Senden Sie Ihre Fragen und Kommentare für Jeffrey Richter in englischer Sprache an mmsync@microsoft.com.

Jeffrey Richter ist Mitbegründer von Wintellect (www.Wintellect.com), einer Firma zur Prüfung von Softwarearchitekturen, Beratung und Schulung. Er ist Autor verschiedener Bücher, darunter CLR via C#. Jeffrey Richter schreibt außerdem redaktionelle Beiträge für das MSDN Magazin und ist seit 1990 als Berater für Microsoft tätig.

Page view tracker