Freigeben über


Asynchrone Aufgaben

Vereinfachte asynchrone Programmierung mit Aufgaben

Igor Ostrovsky

Beispielcode herunterladen.

Das asynchrone Programmieren besteht aus einer Reihe von Techniken für das Implementieren aufwendiger Vorgänge, die parallel mit dem restlichen Programm ausgeführt werden. Ein Bereich, in dem das asynchrone Programmieren häufig vorkommt, ist im Zusammenhang mit Programmen mit einer grafischen Benutzeroberfläche: Die grafische Benutzeroberfläche soll während eines aufwendigen Vorgangs in der Regel nicht "stehen bleiben". Außerdem sind asynchrone Vorgänge bei Serveranwendungen wichtig, bei denen mehrere Clientanforderungen parallel verarbeitet werden müssen.

Zu den repräsentativen Beispielen für asynchrone Vorgänge aus der Praxis zählen das Senden einer Anforderung an einen Server und das Warten auf eine Antwort, das Lesen von Daten von der Festplatte und das Ausführen von rechenintensiven Vorgängen, wie eine Rechtschreibprüfung.

Im Folgenden wird das Beispiel einer Anwendung mit einer Benutzeroberfläche behandelt. Die Anwendung kann mit Windows Presentation Foundation (WPF) oder Windows Forms erstellt werden. In einer solchen Anwendung wird ein Großteil des Codes im Benutzeroberflächenthread ausgeführt, da er die Ereignishandler für Ereignisse ausführt, die von diesen Benutzeroberflächen-Steuerelementen stammen. Wenn der Benutzer auf eine Schaltfläche klickt, nimmt der Benutzeroberflächenthread diese Meldung auf und führt den Click-Ereignishandler aus.

Stellen Sie sich nun vor, dass Ihre Anwendung im Click-Ereignishandler eine Anforderung an einen Server sendet und auf eine Antwort wartet:

// !!! Bad code !!!
void Button_Click(object sender, RoutedEventArgs e) {
  WebClient client = new WebClient();
  client.DownloadFile("https://www.microsoft.com", "index.html");
}

Dieser Code weist ein schwerwiegendes Problem auf: Das Herunterladen einer Website kann mehrere Sekunden oder länger dauern. Es kann auch mehrere Sekunden dauern, bis die Rückmeldung des Aufrufs von Button_Click erfolgt. Der Benutzeroberflächenthread ist also mehrere Sekunden lang blockiert und die Benutzeroberfläche kann nicht bedient werden. Dieses Verhalten ist alles andere als benutzerfreundlich und daher in der Regel nicht zu akzeptieren.

Damit die Anwendungsoberfläche auch bis zur Serverantwort weiterhin reagiert, ist es wichtig, dass der Downloadvorgang kein synchroner Vorgang im Benutzeroberflächenthread ist.

Versuchen wir nun, das Problem der inaktiven Benutzeroberfläche zu beheben. Eine mögliche (aber suboptimale) Lösung ist die Kommunikation mit dem Server über einen anderen Thread, sodass der Benutzeroberflächenthread nicht blockiert wird. Im Folgenden ein Beispiel, das einen Threadpool-Thread für die Kommunikation mit dem Server verwendet:

// Suboptimal code
void Button_Click(object sender, RoutedEventArgs e) {
  ThreadPool.QueueUserWorkItem(_ => {
    WebClient client = new WebClient();
    client.DownloadFile(
      "https://www.microsoft.com", "index.html");
  });
}

In diesem Codebeispiel wird das Problem der ersten Version behoben. Das Button_Click-Ereignis blockiert den Benutzeroberflächenthread zwar nicht, aber die threadbasierte Lösung weist drei schwerwiegende Probleme auf. Im Folgenden werden diese Probleme näher betrachtet.

Problem 1: Verschwendete Threadpool-Threads

In der Lösung, die ich gerade gezeigt habe, wird ein Thread aus dem Threadpool verwendet, um eine Anforderung an den Server zu senden und zu warten, bis der Server antwortet.

Der Threadpool-Thread ist bis zur Serverantwort blockiert. Der Thread kann erst wieder an den Pool zurückgegeben werden, wenn der Aufruf an WebClient.DownloadFile fertig gestellt ist. Das Blockieren eines Threadpool-Threads ist zwar wesentlich günstiger als das Blockieren eines Benutzeroberflächenthreads, da die Benutzeroberfläche dabei nicht blockiert wird, aber ein Thread aus dem Threadpool geht dadurch für andere Vorgänge verloren.

Wenn Ihre Anwendung nur gelegentlich einen Threadpool für eine bestimmte Zeit blockiert, ist der Leistungsverlust wahrscheinlich zu vernachlässigen. Wenn dies bei Ihrer Anwendung aber häufig auftritt, kann ihre Reaktionsfähigkeit aufgrund der Last auf den Threadpool jedoch abnehmen. Der Threadpool erstellt als Reaktion weitere Threads, dies aber auf Kosten der Leistung.

Alle anderen Muster der asynchronen Programmierung, die in diesem Artikel vorgestellt werden, umgehen das Problem des verlorenen Threadpool-Threads.

Problem 2: Ergebnisrückgabe

Es gibt noch eine andere Schwierigkeit bei der Verwendung von Threads für die asynchrone Programmierung: Die Rückgabe von Werten aus einem Helperthreadvorgang ist in der Regel etwas ungeordnet.

Im ersten Beispiel schreibt die DownloadFile-Methode die heruntergeladene Webseite in eine lokale Datei und weist so einen leeren Rückgabewert auf. Betrachten wir nun eine andere Variante des Problems: Anstatt die heruntergeladene Webseite in eine Datei zu schreiben, möchten Sie das empfangene HTML in die Text-Eigenschaft eines Textfelds (mit dem Namen HtmlTextBox) schreiben.

Ein einfacher (und falscher) Ansatz für die Implementierung wäre folgender:

// !!! Broken code !!!
void Button_Click(object sender, RoutedEventArgs e) {
  ThreadPool.QueueUserWorkItem(_ => {
    WebClient client = new WebClient();
    string html = client.DownloadString(
      "https://www.microsoft.com", "index.html");
    HtmlTextBox.Text = html;
  }); 
}

Das Problem ist, dass ein Benutzeroberflächen-Steuerelement (in dem Fall HtmlTextBox) vom Threadpool-Thread geändert wird. Dies ist ein Fehler, da nur der Benutzeroberflächenthread die Benutzeroberfläche ändern darf. Diese Einschränkung besteht sowohl in WPF als auch in Windows Forms aus gutem Grund.

Um dieses Problem zu beheben, können Sie den Synchronisierungskontext im Benutzeroberflächenthread erfassen und dann eine Nachricht über den Threadpool-Thread an ihn senden:

void Button_Click(object sender, RoutedEventArgs e) {
  SynchronizationContext ctx = SynchronizationContext.Current;
  ThreadPool.QueueUserWorkItem(_ => {
    WebClient client = new WebClient();
    string html = client.DownloadString(
      "https://www.microsoft.com");
    ctx.Post(state => {
      HtmlTextBox.Text = (string)state;
    }, html);
  });
}

Es ist wichtig zu erkennen, dass das Problem der Werterückgabe aus einem Helperthread nicht auf Anwendungen mit Benutzeroberflächen beschränkt ist. Die Rückgabe von Werten von einem Thread zu einem anderen ist ein schwieriges Unterfangen, das die Verwendung von Synchronisierungsprimitiven erfordert.

Problem 3: Erstellen asynchroner Vorgänge

Das explizite Arbeiten mit Threads macht auch das Erstellen asynchroner Vorgänge schwierig. Wenn beispielweise mehrere Webseiten parallel heruntergeladen werden sollen, erschwert sich das Schreiben des Synchronisierungscodes noch weiter und wird noch fehleranfälliger.

Eine solche Implementierung würde einen Zähler umfassen, der die Anzahl an asynchronen Vorgängen aufzeichnet. Der Zähler müsste in einer threadsicheren Weise, z. B. durch Verwenden von Interlocked.Decrement, geändert werden. Wenn der Zähler Null anzeigt, würde der Code ausgeführt werden, der die Downloads verarbeitet. All dies führt zu einem nicht gerade trivialen Code, bei dem leicht etwas falsch gemacht werden kann.

Es ist nicht nötig zu erwähnen, dass ein noch komplexeres Erstellungsmuster unter Verwendung des threadbasierten Musters noch schwerer zu implementieren wäre.

Ereignisbasiertes Muster

Ein häufig bei der asynchronen Programmierung mit Microsoft .NET Framework verwendetes Muster ist das ereignisbasierte Modell. Das Ereignismodell beinhaltet eine Methode zum Starten des asynchronen Vorgangs und löst ein Ereignis aus, wenn der Vorgang abgeschlossen wurde.

Das Ereignismuster ist eine Konvention für das Verfügbarmachen von asynchronen Vorgängen, aber kein expliziter Vertrag wie eine Oberfläche. Der Klassenimplementierer kann entscheiden, wie stark er sich an das Muster halten soll. In Abbildung 1 wird ein Beispiel mit Methoden gezeigt, die durch eine korrekte Implementierung der ereignisbasierten asynchronen Programmierungsmuster verfügbar gemacht werden.

Abbildung 1 Methoden für ein ereignisbasiertes Muster

public class AsyncExample {
  // Synchronous methods.
  public int Method1(string param);
  public void Method2(double param);

  // Asynchronous methods.
  public void Method1Async(string param);
  public void Method1Async(string param, object userState);
  public event Method1CompletedEventHandler Method1Completed;

  public void Method2Async(double param);
  public void Method2Async(double param, object userState);
  public event Method2CompletedEventHandler Method2Completed;

  public void CancelAsync(object userState);

  public bool IsBusy { get; }

  // Class implementation not shown.
  ...
}

WebClient ist eine Klasse im .NET Framework, die asynchrone Vorgänge über das ereignisbasierte Muster implementiert. Um eine asynchrone Variante der DownloadString-Methode zur Verfügung zu stellen, werden von WebClient die Methoden "DownloadStringAsync" und "CancelAsync" und das DownloadStringCompleted-Ereignis bereitgestellt. So würde unser Beispiel auf asynchrone Weise implementiert werden:

void Button_Click(object sender, RoutedEventArgs e) {
  WebClient client = new WebClient();
  client.DownloadStringCompleted += eventArgs => {
      HtmlTextBox.Text = eventArgs.Result;
  };
  client.DownloadStringAsync("https://www.microsoft.com");
}

Diese Implementierung löst Problem 1 der ineffizienten threadbasierten Lösung: das unnötige Blockieren von Threads. Der Aufruf von DownloadStringAsync erzeugt unmittelbar einen Rückgabewert und blockiert weder den Benutzeroberflächenthread noch einen Threadpool-Thread. Der Download wird im Hintergrund ausgeführt, und danach wird das DownloadStringCompleted-Ereignis für den entsprechenden Thread ausgeführt.

Beachten Sie, dass der DownloadStringCompleted-Ereignishandler für den entsprechenden Thread ausgeführt wird, ohne dass dafür wie bei der threadbasierten Lösung der SynchronizationContext-Code erforderlich ist. Im Hintergrund erfasst WebClient automatisch den SynchronizationContext und übergibt einen Rückruf an den Kontext. Bei Klassen, die den ereignisbasierten Handler implementieren, wird in der Regel sichergestellt, dass der Completed-Handler für den entsprechenden Thread ausgeführt wird.

Das ereignisbasierte asynchrone Programmierungsmuster ist effizient, da nicht mehr Threads als notwendig blockiert werden, und es ist eines der im .NET Framework am häufigsten verwendeten Muster. Das ereignisbasierte Muster weist jedoch auch mehrere Einschränkungen auf:

  • Das Muster ist informell und nicht festgelegt – Klassen können davon abweichen.
  • Mehrere asynchrone Vorgänge können schwierig zu erstellen sein, wie z. B. das Verarbeiten paralleler asynchroner Vorgänge oder einer Abfolge von asynchronen Vorgängen.
  • Sie können nicht abfragen und prüfen, ob der asynchrone Vorgang abgeschlossen ist.
  • Die Verwendung dieser Typen erfordert große Sorgfalt. Wenn z. B. eine Instanz für die Verarbeitung mehrerer asynchroner Vorgänge verwendet wird, muss ein registrierter Ereignishandler so codiert werden, dass er nur den asynchronen Vorgang verarbeitet, auf den er abzielt, auch wenn er mehrere Male aufgerufen wird.
  • Ereignishandler werden immer für den erfassten SynchronizationContext aufgerufen, wenn der asynchrone Vorgang gestartet wurde, auch wenn die Ausführung für den Benutzeroberflächenthread unnötig ist und zu zusätzlichen Leistungseinbußen führt.
  • Die richtige Implementierung ist unter Umständen schwer und erfordert die Definition mehrerer Typen (z. B. Ereignishandler und Ereignisargumente).

In Abbildung 2 werden mehrere Beispiele von .NET Framework 4-Klassen gezeigt, die das ereignisbasierte asynchrone Muster implementieren.

Abbildung 2 Beispiele für das ereignisbasierte asynchrone Muster in .NET-Klassen

Klasse Vorgang
System.Activities.WorkflowInvoker InvokeAsync
System.ComponentModel.BackgroundWorker RunWorkerAsync
System.Net.Mail.SmtpClient SendAsync
System.Net.NetworkInformation.Ping SendAsync
System.Net.WebClient DownloadStringAsync

IAsyncResult Pattern

Eine andere Konvention für die Implementierung asynchroner Vorgänge in .NET ist das IAsyncResult-Muster. Verglichen mit dem ereignisbasierten Modell ist IAsyncResult eine fortschrittlichere Lösung für das asynchrone Programmieren.

Im IAsyncResult-Muster wird ein asynchroner Vorgang mit den Begin- und End-Methoden zur Verfügung gestellt. Sie rufen die Begin-Methode auf, um den asynchronen Vorgang zu initiieren, und übergeben einen Delegaten, der bei Abschluss des asynchronen Vorgangs aufgerufen wird. Über den Rückruf rufen Sie die End-Methode auf, die wiederum das Ergebnis des asynchronen Vorgangs zurückgibt. Statt einen Rückruf bereitzustellen, können Sie auch abfragen, ob der Vorgang abgeschlossen wurde oder darauf warten.

Denken Sie als Beispiel an die Dns.GetHostAddresses-Methode, die einen Hostnamen akzeptiert und einen Bereich an IP-Adressen zurückgibt, die dem Hostnamen zugeordnet sind. Die Signatur der synchronen Version der Methode sieht etwa folgendermaßen aus:

public static IPAddress[] GetHostAddresses(
  string hostNameOrAddress)
The asynchronous version of the method is exposed as follows:
public static IAsyncResult BeginGetHostAddresses(
  string hostNameOrAddress,
  AsyncCallback requestCallback,
  Object state)

public static IPAddress[] EndGetHostAddresses(
  IAsyncResult asyncResult)

Im Folgenden ein Beispiel, bei dem die Methoden "BeginGetHostAddresses" und "EndGetHostAddresses" verwendet werden, um den DNS auf die Adresse "www.microsoft.com" abzufragen:

static void Main() {
  Dns.BeginGetHostAddresses(
    "www.microsoft.com",
    result => {
      IPAddress[] addresses = Dns.EndGetHostAddresses(result);
      Console.WriteLine(addresses[0]);
    }, 
    null);
  Console.ReadKey();
}

In Abbildung 3 werden mehrere .NET-Klassen gezeigt, die einen asynchronen Vorgang mit dem ereignisbasierten Muster implementieren. Beim Vergleich von Abbildung 2 und 3 werden Sie feststellen, dass einige Klassen das ereignisbasierte Muster implementieren, einige das IAsyncResult-Muster und einige beide.

Abbildung 3 Beispiele für IAsyncResult in .NET-Klassen

Klasse Vorgang
System.Action BeginInvoke
System.IO.Stream BeginRead
System.Net.Dns BeginGetHostAddresses
System.Net.HttpWebRequest BeginGetResponse
System.Net.Sockets.Socket BeginSend
System.Text.RegularExpressions.MatchEvaluator BeginInvoke
System.Data.SqlClient.SqlCommand BeginExecuteReader
System.Web.DefaultHttpHandler BeginProcessRequest

Das IAsyncResult-Muster wurde im .NET Framework 1.0 ursprünglich als hochleistungsfähiger Ansatz für die Implementierung asynchroner APIs eingeführt. Es erfordert jedoch zusätzliche Aufgaben für die Interaktion mit dem Benutzeroberflächenthread, ist schwer zu implementieren und kann schwierig in der Anwendung sein. Das ereignisbasierte Muster wurde im .NET Framework 2.0 eingeführt, um die Benutzeroberflächenaspekte zu behandeln, die von IAsyncResult nicht behandelt werden, und konzentriert sich vor allem auf Szenarien, bei denen eine Benutzeroberflächenanwendung eine einzelne asynchrone Anwendung startet, und arbeitet dann damit.

Aufgabenmuster

Der neue Typ "System.Threading.Tasks.Task" wurde in .NET Framework 4 als Methode für die Darstellung asynchroner Vorgänge eingeführt. Eine Aufgabe kann eine einfache Berechnung sein, die auf einem Prozessor ausgeführt wird:

static void Main() {
  Task<double> task = Task.Factory.StartNew(() => { 
    double result = 0; 
    for (int i = 0; i < 10000000; i++) 
      result += Math.Sqrt(i);
    return result;
  });

  Console.WriteLine("The task is running asynchronously...");
  task.Wait();
  Console.WriteLine("The task computed: {0}", task.Result);
}

Aufgaben, die mit der StartNew-Methode erstellt werden, entsprechen den Aufgaben, die standardmäßig Code für den Threadpool ausführen. Aufgaben sind jedoch allgemeiner und können willkürliche asynchrone Vorgänge wiedergeben – auch solche, die z. B. für die Kommunikation mit einem Server oder das Lesen von Daten von der Festplatte stehen.

TaskCompletionSource ist der allgemeine Mechanismus für das Erstellen von Aufgaben, die asynchrone Vorgänge wiedergeben. TaskCompletionSource ist mit genau einer Aufgabe verknüpft. Nachdem die SetResult-Methode für TaskCompletionSource aufgerufen wurde, wird die verknüpfte Aufgabe abgeschlossen und gibt den Ergebniswert der Aufgabe (siehe Abbildung 4) zurück.

Abbildung 4 Verwenden von TaskCompletionSource

static void Main() {
  // Construct a TaskCompletionSource and get its 
  // associated Task
  TaskCompletionSource<int> tcs = 
    new TaskCompletionSource<int>();
  Task<int> task = tcs.Task;

  // Asynchronously, call SetResult on TaskCompletionSource
  ThreadPool.QueueUserWorkItem( _ => {
    Thread.Sleep(1000); // Do something
    tcs.SetResult(123);
  });

  Console.WriteLine(
    "The operation is executing asynchronously...");
  task.Wait();

  // And get the result that was placed into the task by 
  // the TaskCompletionSource
  Console.WriteLine("The task computed: {0}", task.Result);
}

Hier wurde ein Threadpool-Thread für den Aufruf von SetResult für TaskCompletionSource verwendet. Es ist jedoch wichtig zu beachten, dass die SetResult-Methode von jedem Code aufgerufen werden kann, der auf TaskCompletionSource zugreifen kann – ein Ereignishandler für ein Button.Click-Ereignis, eine Aufgabe, die eine Berechnung abgeschlossen hat, ein Ereignis, das ausgelöst wurde, da ein Server auf eine Anforderung reagiert hat und so weiter.

TaskCompletionSource ist also ein sehr allgemeiner Mechanismus für das Implementieren von asynchronen Vorgängen.

Konvertieren eines IAsyncResult-Musters

Um Aufgaben für die asynchrone Programmierung verwenden zu können, muss die Interoperabilität mit asynchronen Vorgängen gegeben sein, die von älteren Methoden zur Verfügung gestellt werden. Während TaskCompletionSource jeden asynchronen Vorgang einbinden und ihn als Aufgabe verfügbar machen kann, bietet die Aufgaben-API eine bequeme Methode zum Konvertieren von IAsyncResult als Aufgabe: die FromAsync-Methode.

In diesem Beispiel wird die FromAsync-Methode zum Konvertieren des auf IAsync­Result basierenden asynchronen Vorgangs Dns.BeginGetHost­ Addresses in eine Aufgabe verwendet:

static void Main() {
  Task<IPAddress[]> task = 
    Task<IPAddress[]>.Factory.FromAsync(
      Dns.BeginGetHostAddresses, 
      Dns.EndGetHostAddresses,
      "https://www.microsoft.com", null);
  ...
}

FromAsync erleichtert die Konvertierung der asynchronen IAsyncResult-Vorgänge als Aufgaben. Im Hintergrund wird FromAsync auf ähnliche Weise implementiert wie im Beispiel für TaskCompletionSource, bei dem ThreadPool verwendet wurde. Hier eine einfache Annäherung der Implementierung; in diesem Fall wird GetHostAddresses direkt angesprochen:

static Task<IPAddress[]> GetHostAddressesAsTask(
  string hostNameOrAddress) {

  var tcs = new TaskCompletionSource<IPAddress[]>();
  Dns.BeginGetHostAddresses(hostNameOrAddress, iar => {
    try { 
      tcs.SetResult(Dns.EndGetHostAddresses(iar)); }
    catch(Exception exc) { tcs.SetException(exc); }
  }, null);
  return tcs.Task;
}

Konvertieren eines ereignisbasierten Musters

Ereignisbasierte asynchrone Vorgänge können auch mit der TaskCompletionSource-Klasse in Aufgaben konvertiert werden. Die Aufgabenklasse stellt keinen integrierten Mechanismus für diese Konvertierung zur Verfügung – ein allgemeiner Mechanismus ist unpraktisch, da das ereignisbasierte asynchrone Muster nur eine Konvention ist.

Im Folgenden wird gezeigt, wie ein ereignisbasierter asynchroner Vorgang in eine Aufgabe konvertiert wird. Das Codebeispiel zeigt eine Methode, bei der ein URI verwendet und eine Aufgabe zurückgegeben wird, die den asynchronen Vorgang WebClient.DownloadStringAsync darstellt:

static Task<string> DownloadStringAsTask(Uri address) {
  TaskCompletionSource<string> tcs = 
    new TaskCompletionSource<string>();
  WebClient client = new WebClient();
  client.DownloadStringCompleted += (sender, args) => {
    if (args.Error != null) tcs.SetException(args.Error);
    else if (args.Cancelled) tcs.SetCanceled();
    else tcs.SetResult(args.Result);
  };
  client.DownloadStringAsync(address);
  return tcs.Task;
}

Mit diesem Muster und dem Muster im vorigen Abschnitt können Sie bestehende asynchrone Muster – ereignisbasiert oder IAsyncResult-basiert – in eine Aufgabe konvertieren.

Ändern und Zusammenstellen von Aufgaben

Sie fragen sich, warum Sie Aufgaben zur Darstellung asynchroner Vorgänge verwenden sollten? Der Hauptgrund ist die Tatsache, dass asynchrone Vorgänge mit Aufgaben einfach geändert und zusammengestellt werden können. Im Gegensatz zu IAsyncResult und ereignisbasierten Ansätzen stellt eine Aufgabe ein einzelnes Objekt bereit, das alle wichtigen Informationen zu dem asynchronen Vorgang und zur Vorgehensweise für die Verknüpfung damit sowie zum Abrufen des Ergebnisses usw. enthält.

Ein Vorteil ist, dass der Abschluss einer Aufgabe abgewartet werden kann. Sie können den Abschluss einer oder aller Aufgaben in einer Aufgabenfolge oder den Abschluss einer bestimmten Aufgabe in einer Aufgabenfolge abwarten.

static void Main() {
  Task<int> task1 = new Task<int>(() => ComputeSomething(0));
  Task<int> task2 = new Task<int>(() => ComputeSomething(1));
  Task<int> task3 = new Task<int>(() => ComputeSomething(2));

  task1.Wait();
  Console.WriteLine("Task 1 is definitely done.");

  Task.WaitAny(task2, task3);
  Console.WriteLine("Task 2 or task 3 is also done.");

  Task.WaitAll(task1, task2, task3);
  Console.WriteLine("All tasks are done.");
}

Eine weitere nützliche Funktion von Aufgaben ist, dass Fortsetzungen geplant werden können. Dabei handelt es sich um Aufgaben, die nach dem Abschluss einer anderen Aufgabe ausgeführt werden. Ähnlich wie beim Warten können Sie Fortsetzungen planen, die nach dem Abschluss einer bestimmten Aufgabe oder aller Aufgaben in einer Aufgabenfolge bzw. nach Abschluss einer bestimmten Aufgabe in einer Aufgabenfolge ausgeführt werden.

In diesem Beispiel wird eine Aufgabe zur Abfrage des DNS für die Adresse "www.microsoft.com" erstellt. Nachdem die Aufgabe abgeschlossen ist, wird die Fortsetzungsaufgabe gestartet. Das Ergebnis wird in die Konsole ausgegeben:

static void Main() {
  Task<IPAddress[]> task = 
    Task<IPAddress[]>.Factory.FromAsync(
      Dns.BeginGetHostAddresses, 
      Dns.EndGetHostAddresses,
      "www.microsoft.com", null);

  task.ContinueWith(t => Console.WriteLine(t.Result));
  Console.ReadKey();
}

Wenden wir uns jetzt einigen interessanteren Beispielen zu, die das Potenzial der Aufgabe als Darstellung eines asynchronen Vorgangs belegen. Abbildung 5 zeigt ein Beispiel, in dem zwei DNS-Suchvorgänge parallel ausgeführt werden. Wenn die asynchronen Vorgänge als Aufgaben dargestellt werden, kann problemlos auf den Abschluss mehrerer Vorgänge gewartet werden.

Abbildung 5 Parallele Ausführung von Vorgängen

static void Main() {
  string[] urls = new[] { "www.microsoft.com", "www.msdn.com" };
  Task<IPAddress[]>[] tasks = new Task<IPAddress[]>[urls.Length];

  for(int i=0; i<urls.Length; i++) {
    tasks[i] = Task<IPAddress[]>.Factory.FromAsync(
      Dns.BeginGetHostAddresses,
      Dns.EndGetHostAddresses,
      urls[i], null);
  }

  Task.WaitAll(tasks);

  Console.WriteLine(
    "microsoft.com resolves to {0} IP addresses. msdn.com resolves to {1}",
    tasks[0].Result.Length,
    tasks[1].Result.Length);
}

Sehen wir uns als Nächstes ein Beispiel zum Zusammenstellen von Aufgaben an, das aus den folgenden drei Schritten besteht:

  1. Asynchrones Paralleles Herunterladen mehrerer HTML-Seiten
  2. Verarbeiten der HTML-Seiten
  3. Zusammenfassen der Informationen aus den HTML-Seiten

In Abbildung 6 wird dargestellt, wie Berechnungen dieser Art durch die weiter oben in diesem Artikel gezeigte DownloadStringAsTask-Methode implementiert werden. Ein hervorstechender Vorteil dieser Implementierung besteht darin, dass die zwei unterschiedlichen CountParagraphs-Methoden für verschiedene Threads ausgeführt werden. In Anbetracht der aktuellen Verbreitung von Mehrkerncomputern profitieren Programme, die rechenintensive Arbeiten auf mehrere Threads verteilen, von entscheidenden Leistungsvorteilen.

Abbildung 6 Asynchrones Herunterladen von Zeichenfolgen

static void Main() {
  Task<string> page1Task = DownloadStringAsTask(
    new Uri("https://www.microsoft.com"));
  Task<string> page2Task = DownloadStringAsTask(
    new Uri("http://www.msdn.com"));

  Task<int> count1Task = 
    page1Task.ContinueWith(t => CountParagraphs(t.Result));
  Task<int> count2Task = 
    page2Task.ContinueWith(t => CountParagraphs(t.Result));

  Task.Factory.ContinueWhenAll(
    new[] { count1Task, count2Task },
    tasks => {
      Console.WriteLine(
        "<P> tags on microsoft.com: {0}", 
        count1Task.Result);
      Console.WriteLine(
        "<P> tags on msdn.com: {0}", 
        count2Task.Result);
  });
        
  Console.ReadKey();
}

Ausführen von Aufgaben in einem Synchronisierungskontext

Manchmal kann es hilfreich sein, eine Fortführung zu planen, die in einem bestimmten Synchronisierungskontext ausgeführt wird. In Anwendungen mit einer Benutzeroberfläche etwa ist es von Vorteil, eine Fortführung planen zu können, die für den Benutzeroberflächenthread ausgeführt wird.

Eine Aufgabe kann am einfachsten mit einem Synchronisierungskontext interagieren, wenn ein TaskScheduler erstellt wird, der den Kontext des aktuellen Threads aufzeichnet. Einen TaskScheduler für den Benutzeroberflächenthread erhalten Sie durch Aufruf der statischen FromCurrentSynchronizationContext-Methode für den TaskScheduler-Typ während der Ausführung des Benutzeroberflächenthreads.

In diesem Beispiel wird die Webseite "www.microsoft.com" heruntergeladen. Danach werden die heruntergeladenen HTML-Daten der Text-Eigenschaft eines WPF-Textfelds zugewiesen:

void Button_Click(object sender, RoutedEventArgs e) {
  TaskScheduler uiTaskScheduler =
    TaskScheduler.FromCurrentSynchronizationContext()

  DownloadStringAsTask(new Uri("https://www.microsoft.com"))
    .ContinueWith(
       t => { textBox1.Text = t.Result; },
       uiTaskScheduler);
}

Mit dem Text der Button_Click-Methode wird die asynchrone Berechnung eingerichtet, die schließlich die UI aktualisiert. Button_Click wartet jedoch nicht auf den Abschluss der Berechnung. Auf diese Weise wird der Benutzeroberflächenthread nicht blockiert, sondern kann weiter die Benutzeroberfläche aktualisieren und auf Benutzeraktionen reagieren.

Wie bereits an früherer Stelle erwähnt, wurden vor .NET Framework 4 asynchrone Vorgänge normalerweise mit dem IAsyncResult-Muster oder dem ereignisbasierten Muster bereitgestellt. Mit .NET Framework 4 können Sie jetzt die Aufgabenklasse als eine weitere nützliche Darstellung von asynchronen Vorgängen einsetzen. Bei der Darstellung als Aufgabe können asynchrone Vorgänge oft einfacher geändert und zusammengestellt werden. Weitere Beispiele zur Verwendung von Aufgaben für die asynchrone Programmierung finden Sie in den ParallelExtensionsExtras-Beispielen, die unter code.msdn.microsoft.com/ParExtSamples heruntergeladen werden können.

Igor Ostrovsky ist Softwareentwickler im Parallel Computing Platform-Team von Microsoft. Ostrovsky dokumentiert seine Programmierabenteuer unter igoro.com und trägt zum Blog Parallel Programming with .NET unter blogs.msdn.com/pfxteam bei.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Concurrency Runtime-Team