Oktober 2014

Volume 29 Number 10

Async Programming - Introduction to Async/Await on ASP.NET

Stephen Cleary | Oktober 2014

Die meisten Online-Ressourcen zu Async/Await gehen davon aus, dass Sie Client-Anwendungen entwickeln. Aber gibt es auch einen Platz für Async auf dem Server? Die Antwort ist definitiv „Ja“. Dieser Artikel gibt eine Einführung in asynchrone Anforderungen auf ASP.NET und verweist auf die besten Onlineressourcen. Ich werde nicht auf die Async- oder Await-Syntax eingehen; dieses Thema habe ich bereits in einem einführenden Blogeintrag (bit.ly/19IkogW) sowie in einem Artikel zu bewährten Async-Methoden (msdn.microsoft.com/magazine/jj991977) behandelt. Dieser Artikel geht vor allem darauf ein, wie Async auf ASP.NET funktioniert.

Für Clientanwendungen wie Windows Store, Windows-Desktop-Apps und Windows Phone-Apps liegt der größte Vorteil von Async in dessen Reaktionsfähigkeit. Diese Arten von Apps verwenden Async vor allem, um die Benutzeroberfläche reaktionsfähig zu machen. Für Serveranwendungen liegt der Vorteil von Async vor allem in der Skalierbarkeit. Die Skalierbarkeit von Node.js liegt in dessen genereller asynchroner Natur; Open Web Interface for .NET (OWIN) wurde von Grund auf asynchron entwickelt, und ASP.NET kann ebenfalls asynchron sein. Async: Nicht nur für Anwendungsbenutzeroberflächen!

Synchrone und asynchrone Anforderungsverarbeitung

Bevor es um asynchrone Anforderungshandler geht, möchte ich kurz darauf eingehen, wie synchrone Anforderungshandler auf ASP.NET funktionieren. Für dieses Beispiel gehen wir davon aus, dass die Anforderungen im System von einer externen Ressource abhängen, wie etwa einer Datenbank oder Web-API. Wenn eine Anforderung eingeht, nimmt ASP.NET einen seiner Threadpoolthreads und weist diesen der Anforderung zu. Da dieser synchron geschrieben ist, wird der Anforderungshandler diese externe Ressource synchron aufrufen. Dadurch wird der Anforderungsthread blockiert, bis der Aufruf zur externen Ressource zurückkommt. Abbildung 1 zeigt einen Threadpool mit zwei Threads, von denen einer geblockt ist und auf eine externe Ressource wartet.

Synchrones Warten auf eine externe Ressource
Abbildung 1 Synchrones Warten auf eine externe Ressource

Dieser externe Ressourcenaufruf kommt schließlich zurück, und der Anforderungsthread nimmt die Bearbeitung dieser Anforderung wieder auf. Wenn die Anforderung abgeschlossen ist und die Antwort gesendet werden kann, wird der Anforderungsthread zum Threadpool zurückgegeben.

Dies ist schön und gut, solange der ASP.NET-Server nicht mehr Anforderungen erhält, als er Threads für deren Verarbeitung besitzt. An dieser Stelle müssen die zusätzlichen Anforderungen warten, bis ein Thread verfügbar ist, bevor sie ausgeführt werden können. Abbildung 2 zeigt denselben Server mit zwei Threads, wenn dieser drei Anforderungen empfängt.

Ein Server mit zwei Threads, der drei Anforderungen empfängt
Abbildung 2 Ein Server mit zwei Threads, der drei Anforderungen empfängt

In dieser Situation werden den ersten beiden Anforderungen Threads aus dem Threadpool zugewiesen. Jede dieser Anforderungen ruft eine externe Ressource auf, und blockiert deren Threads. Die dritte Anforderung muss warten, bis ein Thread verfügbar ist, bevor sie mit der Verarbeitung beginnen kann, aber die Anforderung befindet sich bereits im System. Der Timer läuft, und die Gefahr von HTTP-Fehler 503 (Dienst nicht verfügbar) besteht.

Aber ziehen wir Folgendes in Betracht: Diese dritte Anforderung wartet auf einen Thread, während zwei andere Threads im System effektiv nichts tun. Diese Threads sind nur blockiert und warten darauf, dass der externe Aufruf zurückkehrt. Sie sind nicht wirklich beschäftigt, werden nicht ausgeführt und nehmen keine CPU-Zeit in Anspruch. Diese Threads werden verschwendet, während eine Anforderung Bedarf hat. Dies ist eine Situation, die mit asynchronen Anforderungen gelöst werden kann.

Asynchrone Anforderungshandler gehen anders vor. Wenn eine Anforderung eingeht, nimmt ASP.NET einen seiner Threadpoolthreads und weist diesen der Anforderung zu. Dieses Mal wird der Anforderungshandler diese externe Ressource asynchron aufrufen. Dadurch gelangt der Anforderungsthread zurück zum Threadpool, bis der Aufruf zur externen Ressource zurückkommt. Abbildung 3 zeigt den Threadpool mit zwei Threads, während die Anforderung asynchron auf die externe Ressource wartet.

Asynchrones Warten auf eine externe Ressource
Abbildung 3 Asynchrones Warten auf eine externe Ressource

Der wichtige Unterschied liegt darin, dass der Anforderungsthread zum Threadpool zurückgegeben wurde, während der asynchrone Aufruf läuft. Auch wenn sich der Thread im Threadpool befindet, ist er nicht mehr mit dieser Anforderung verbunden. Wenn der externe Ressourcenaufruf zurückkehrt, nimmt ASP.NET in diesem Fall einen der Threadpoolthreads und weist ihn erneut dieser Anforderung zu. Dieser Thread fährt dann mit der Verarbeitung dieser Anforderung fort. Wenn die Anforderung abgeschlossen ist, wird dieser Thread erneut zum Threadpool zurückgegeben. Bei synchronen Handlern wird derselbe Thread für die gesamte Lebenszeit der Anforderung verwendet. Bei asynchronen Handlern werden dagegen möglicherweise verschiedene Threads zur selben Anforderung (zu verschiedenen Zeiten) zugewiesen.

Wenn jetzt drei Anforderungen eingehen, könnte der Server einfach damit umgehen. Da die Threads im Threadpool freigegeben werden, wenn die Anforderung auf asynchrone Arbeit wartet, können diese neue sowie vorhandene Anforderungen verarbeiten. Asynchrone Anforderungen ermöglichen, dass eine kleinere Anzahl Threads eine größere Anzahl an Anforderungen verarbeiten kann. Der größte Vorteil von asynchronem Code auf ASP.NET ist jedoch die Skalierbarkeit.

Warum nicht einfach den Threadpool vergrößern?

An diesem Punkt wird immer folgende Frage gestellt: Warum vergrößern wir nicht einfach den Threadpool? Dafür gibt es zwei Gründe: Mithilfe von asynchronem Code kann weiter und schneller skaliert werden als durch das Blockieren von Threadpoolthreads.

Asynchroner Code kann weiter skalieren, als es durch die Threadblockierung möglich ist, da dies weniger Speicherplatz verbraucht. Jeder Threadpoolthread auf einem modernen Betriebssystem hat einen Stapel von 1MB, plus einen nicht auslagerbaren Kernelstapel. Das hört sich nach nicht viel an, außer wenn Sie einen ganzen Satz an Threads auf Ihrem Server haben. Der Mehraufwand an Speicherplatz ist im Gegensatz dazu bei einem asynchronen Vorgang deutlich kleiner. Eine Anforderung mit einem asynchronen Vorgang hat also deutlich weniger Speicherplatzdruck als eine Anforderung mit einem blockierten Thread. Asynchroner Code ermöglicht Ihnen, mehr Speicherplatz für andere Dinge zu verwenden (zum Beispiel für das Zwischenspeichern).

Durch asynchronen Code können Sie schneller skalieren als durch die Threadblockierung, da der Threadpool eine eingeschränkte Injektionsrate hat. Als der Artikel geschrieben wurde, betrug die Rate ein Thread alle zwei Sekunden. Diese Injektionsratenbeschränkung ist eine gute Sache, da damit eine konstante Threaderstellung und -zerstörung vermieden wird. Überlegen Sie einmal, was passieren würde, wenn eine plötzliche Anforderungsflut eingeht. Synchroner Code kann einfach ins Stocken geraten, wenn die Anforderungen alle verfügbaren Threads verbrauchen und die verbleibenden Anforderungen darauf warten müssen, dass der Threadpool neue Threads injiziert. Asynchroner Code hat dagegen keine solche Einschränkung, dieser ist allseits bereit. Asynchroner Code kann leichter auf plötzliche Änderungen des Anforderungsvolumens reagieren.

Denken Sie jedoch daran, dass asynchroner Code nicht den Threadpool ersetzt. Es geht nicht um die Frage Threadpool oder asynchroner Code; es heißt Threadpool und asynchroner Code. Asynchroner Code ermöglicht der Anwendung, den Threadpool optimal zu nutzen. Diese verwendet den vorhandenen Threadpool.

Was ist mit dem Thread, der die asynchrone Arbeit erledigt?

Mir wird diese Frage ständig gestellt. Die Intention dahinter ist, dass es irgendwo einen Thread geben muss, der den E/A-Aufruf zur externen Ressource blockiert. Asynchroner Code befreit also den Anforderungsthread, aber nur zum Preis eines anderen Threads an einer anderen Stelle des Systems, nicht wahr? Nein, das ist nicht richtig.

Um zu erklären, warum asynchrone Anforderungen skalieren, werde ich ein (vereinfachtes) Beispiel eines asynchronen E/A-Aufrufs erläutern. Lassen Sie uns davon ausgehen, dass eine Anforderung etwas auf eine Datei schreiben muss. Der Anforderungsthread ruft die asynchrone Schreibmethode auf. WriteAsync wird durch die Basisklassenbibliothek (Base Class Library, BCL) implementiert und verwendet Komplettierungsports für die asynchronen E/A-Vorgänge. Somit wird der WriteAsync-Aufruf an das Betriebssystem als asynchroner Dateischreibvorgang weitergegeben. Das Betriebssystem kommuniziert dann mit dem Treiberstapel und gibt die Daten weiter, sodass diese in ein E/A-Anforderungspaket (IRP) geschrieben werden.

An dieser Stelle wird es interessant: Wenn ein Gerätetreiber ein IRP nicht sofort verarbeiten kann, muss er dieses asynchron bearbeiten. Der Treiber teilt dem Datenträger somit mit, dass mit dem Schreiben begonnen werden kann, und gibt die Antwort „ausstehend“ an das Betriebssystem weiter. Das Betriebssystem gibt diese „ausstehend“-Antwort an die BCL weiter, und die BCL gibt eine unvollständige Aufgabe zum Code weiter, welcher die Anforderung verarbeitet. Der Code, welcher die Anforderung verarbeitet, wartet auf die Aufgabe, welche eine unvollständige Aufgabe von dieser Methode zurückgibt, usw. Schließlich gibt der Code, welcher die Anforderung verarbeitet, eine unvollständige Aufgabe an ASP.NET zurück, und der Anforderungsthread wird zum Threadpool freigegeben.

Denken Sie nun an den aktuellen Systemstatus. Es gibt verschiedene E/A-Strukturen, die zugeteilt wurden (zum Beispiel Aufgabeninstanzen und IRP), und diese befinden sich alle im Status ausstehend/unvollständig. Es gibt jedoch keinen Thread, der mit dem Warten auf die Fertigstellung des Schreibvorgangs blockiert ist. Weder ASP.NET, BCL, Betriebssystem noch Gerätetreiber haben einen Thread speziell für asynchrone Vorgänge.

Wenn der Datenträger die Daten fertig geschrieben hat, benachrichtigt er den Treiber per Unterbrechung. Der Treiber informiert das Betriebssystem, dass das IRP abgeschlossen ist, und das Betriebssystem benachrichtigt die BLC über den Komplettierungsport. Ein Threadpoolthread antwortet auf diese Benachrichtigung, indem die Aufgabe abgeschlossen wird, die von WriteAsync zurückgegeben wurde. Dadurch wird der asynchrone Anforderungscode fortgesetzt. Während der Abschluss- und Benachrichtigungsphase wurden einige Threads kurz „ausgeliehen“, aber kein Thread wurde während des Schreibvorgangs wirklich blockiert.

Dieses Beispiel ist stark vereinfacht, zeigt aber den wichtigsten Punkt: es wird kein Thread für die wahrhaft asynchronen Vorgänge benötigt. Es ist keine CPU-Zeit erforderlich, um die Bytes hinauszuschieben. Es gibt eine zweite Lektion. Überlegen Sie einmal, wie ein Gerätetreiber ein IRP entweder sofort oder asynchron behandeln muss. Eine synchrone Behandlung ist keine Option. Auf Gerätetreiberebene verlaufen alle nicht trivialen E/A-Vorgänge asynchron. Viele Entwickler glauben, dass die „natürliche API“ für E/A-Vorgänge synchron ist, wobei die asynchrone API als Schicht auf der natürlichen, synchronen API liegt. Das ist jedoch überhaupt nicht der Fall: Die natürliche API ist asynchron; und die synchronen APIs, die implementiert sind, verwenden asynchrone E/A-Vorgänge!

Warum gab es bis jetzt noch keine asynchronen Handler?

Warum ist die asynchrone Anforderungsverarbeitung erst jetzt verfügbar, wenn sie so wunderbar ist? Asynchroner Code bietet eine so gute Skalierbarkeit, dass die ASP.NET-Plattform asynchrone Handler und Module seit Beginn von Microsoft .NET Framework unterstützt hat. Asynchrone Webseiten wurden in ASP.NET 2.0 eingeführt, und MVC erhielt asynchrone Controller in ASP.NET MVC 2.

Bis vor kurzem war asynchroner Code jedoch schwierig zu erstellen und zu pflegen. Viele Unternehmen entschieden, dass es einfacher sei, den Code synchron zu entwickeln und für größere Serverfarmen oder teureres Hosting zu bezahlen. Nun hat sich das Blatt gewendet: in ASP.NET 4.5 ist asynchroner Code, der Async und Await verwendet, fast so einfach zu erstellen wie synchroner Code. Da große Systeme in Cloudhosting verschoben werden und mehr Skalierung erfordern, nutzen immer mehr Unternehmen Async und Await auf ASP.NET.

Asynchroner Code ist keine Wunderwaffe

Auch wenn asynchrone Anforderungsverarbeitung wunderbar ist, wird dies nicht all Ihre Probleme lösen können. Es gibt einige allgemein verbreitete Missverständnisse darüber, was Async und Await auf ASP.NET tun können.

Wenn einige Entwickler von Async und Await hören, glauben sie, dass dies eine Möglichkeit für Servercode ist, der dem Client „ausgegeben“ wird (zum Beispiel der Browser). Async und Await auf ASP.NET geben nur zu ASP.NET-Laufzeit aus; das HTTP-Protokoll bleibt unverändert und Sie erhalten weiterhin eine Antwort pro Anforderung. Falls Sie vor Async/Await SignalR oder AJAX oder UpdatePanel benötigten, benötigen Sie nach Async/Await weiterhin SignalR oder AJAX oder UpdatePanel.

Asynchrone Anforderungsverarbeitung mit Async und Await kann bei der Skalierung Ihrer Anwendungen helfen. Dies ist jedoch eine Skalierung auf einem einzelnen Server. Möglicherweise planen Sie ja eine Erweiterung. Wenn Sie eine Erweiterungsarchitektur benötigen, müssen Sie weiterhin statusfreie, idempotente Anforderungen und zuverlässige Warteschlangen berücksichtigen. Async und Await helfen folgendermaßen: Sie ermöglichen Ihnen, vollständig von Ihren Serverressourcen zu profitieren, sodass Sie diese nicht so oft erweitern müssen. Aber falls Sie dennoch erweitern müssen, benötigen Sie eine richtig verteilte Architektur.

Async und Await auf ASP.NET drehen sich um E/A-Vorgänge. Sie zeichnen sich durch das Lesen und Schreiben von Dateien, Datenbankeinträgen und REST-APIs aus. Sie eignen sich jedoch nicht für CPU-gebundene Aufgaben. Sie können einige Hintergrundarbeit anstoßen, indem Sie Task.Run abwarten, aber dafür gibt es keinen Grund. Dies wird nämlich die Skalierbarkeit verschlechtern, da es ASP.NET-Threadpoolheuristiken beeinträchtigt. Wenn Sie CPU-gebundene Arbeit auf ASP.NET haben, ist die beste Lösung, diese direkt auf dem Anforderungsthread auszuführen. Eine generelle Regel ist, dass keine Arbeit auf dem Threadpool auf ASP.NET in die Warteschlange soll.

Betrachten Sie zuletzt die Skalierbarkeit Ihres System als Ganzes. Vor zehn Jahren war die normale Architektur ein ASP.NET-Webserver, der mit einem SQL Server-Datenbank-Back-End kommunizierte. In dieser einfachen Architektur ist normalerweise der Datenbankserver der Skalierbarkeitsengpass, und nicht der Webserver. Ihre Datenbankaufrufe asynchron zu machen, würde wahrscheinlich nicht helfen. Sie könnten diese sicherlich verwenden, um den Webserver zu skalieren, aber der Datenbankserver wird verhindern, dass das gesamte System skaliert.

Rick Anderson stellt den Fall gegen asynchrone Datenbankaufrufe in seinem exzellenten Blogeintrag „Should My Database Calls Be Asynchronous? (Sollen meine Datenbankaufrufe asynchron sein?)“ dar. (bit.ly/1rw66UB). Es gibt zwei Argumente, die dies unterstützen: Erstens ist asynchroner Code kompliziert (und daher teuer an Entwicklerzeit im Vergleich zum Kauf größerer Server), und zweitens ist die Skalierung des Webservers nicht sehr sinnvoll, wenn das Datenbank-Back-End den Engpass darstellt. Beide dieser Argumente hatten absolut ihre Berechtigung, als dieser Beitrag verfasst wurde, aber die beiden Argumente sind im Laufe der Zeit weniger gewichtig geworden. Erstens ist asynchroner Code mit Async und Await deutlich einfacher zu verfassen. Zweitens skalieren die Daten-Back-Ends für Websites, da die Welt sich dem Cloudcomputing zuwendet. Moderne Back-Ends wie Microsoft Azure SQL Database, NoSQL und andere APIs können weiter als ein einzelner SQL Server skalieren, wodurch der Engpass zurück zum Webserver gelangt. In diesem Szenario kann Async/Await durch die Skalierung von ASP.NET enorme Vorteile bieten.

Vor Beginn

Das erste, was Sie wissen müssen, ist, dass Async und Await nur auf ASP.NET 4.5 unterstützt werden. Es gibt ein NuGet-Paket namens Microsoft.Bcl.Async, das Async und Await für .NET Framework 4 ermöglicht. Sie sollten dieses aber nicht verwenden, es wird nicht richtig funktionieren! Der Grund liegt darin, dass ASP.NET selbst ändern musste, wie es deren asynchrone Anforderungsverarbeitung verwaltet, damit diese besser mit Async und Await funktioniert; das NuGet-Paket enthält alle Typen, die der Compiler benötigt, wird aber nicht die ASP.NET-Laufzeit reparieren. Es gibt keine Umgehungslösung, Sie benötigen ASP.NET 4.5 oder höher.

Sie sollten als Nächstes erfahren, dass ASP.NET 4.5 für einen Quirksmodus auf dem Server sorgt. Wenn Sie ein neues ASP.NET 4.5-Projekt erstellen, gibt es keine Probleme. Wenn Sie jedoch ein vorhandenes Projekt auf ASP.NET 4.5 heraufstufen, gibt es einige Probleme. Ich empfehle Ihnen, dies zu vermeiden, indem Sie web.config bearbeiten und httpRuntime.targetFramework auf 4.5 festlegen. Wenn Ihre Anwendung mit dieser Einstellung Probleme hat (und Sie keine Zeit aufwenden möchten, um diese zu beheben), können Sie zumindest Async/Await zum Laufen bringen, indem Sie einen appSetting-Schlüssel aspnet:UseTaskFriendlySynchronizationContext mit Wert „true“ hinzufügen. Der appSetting-Schlüssel ist nicht erforderlich, wenn Sie httpRuntime.targetFramework auf 4.5 festgelegt haben. Das Webentwicklungsteam hat einen Blogeintrag zu den Details dieses neuen Quirksmodus unter bit.ly/1pbmnzK erstellt. Tipp: Wenn Sie altes Verhalten oder Ausnahmen sehen und Ihr Aufrufstapel LegacyAspNetSynchronizationContext enthält, läuft Ihre Anwendung in diesem Quirksmodus. LegacyAspNet­SynchronizationContext ist nicht mit Async kompatibel; Sie benötigen das reguläre AspNetSynchronizationContext auf ASP.NET 4.5.

In ASP.NET 4.5 haben alle ASP.NET-Einstellungen gute Standardwerte für asynchrone Anforderungen, aber es gibt einige weitere Einstellungen, die Sie möglicherweise ändern möchten. Die erste ist eine IIS-Einstellung: Sie sollten überlegen, die IIS/HTTP.sys-Warteschlangenbeschränkung (Anwendungspools | Erweiterte Einstellungen | Warteschlangenlänge) vom Standardwert 1.000 auf 5.000 zu setzen. Die nächste ist eine .NET-Laufzeiteinstellung: ServicePointManager.DefaultConnectionLimit, das als Standardwert die zwölffache Kernanzahl hat. DefaultConnectionLimit begrenzt die Anzahl simultaner ausgehender Verbindungen zum selben Hostnamen.

Ein Wort zum Abbruch von Anforderungen

Wenn ASP.NET eine Anforderung synchron verarbeitet, hat es einen sehr einfachen Mechanismus für den Abbruch von Anforderungen (zum Beispiel bei einer Zeitüberschreitung der Anforderung): Es wird den Workerthread für diese Anforderung abbrechen. Dies ist bei synchronen Prozessen sinnvoll, wo jede Anforderung von Anfang bis Ende denselben Workerthread hat. Der Abbruch von Threads ist nicht sehr gut für die langfristige Stabilität von AppDomain, sodass ASP.NET Ihre Anwendung regelmäßig wiederverwerten wird, um sie funktional zu halten.

Bei asynchronen Anforderungen wird ASP.NET keine Workerthreads abbrechen, wenn es eine Anforderung anhalten möchte. Stattdessen wird es die Anforderung mit einem CancellationToken abbrechen. Asynchrone Anforderungshandler sollten Abbruchtoken akzeptieren und berücksichtigen. Die meisten neueren Frameworks (einschließlich Web API, MVC und SignalR) werden einen CancellationToken erstellen und direkt an Sie weitergeben. Sie müssen diesen dann nur noch als Parameter deklarieren. Sie können auch direkt auf ASP.NET-Token zugreifen, zum Beispiel auf HttpRequest.TimedOutToken ist ein CancellationToken, der für einen Abbruch nach einer Zeitüberschreitung sorgt.

Da immer mehr Anwendungen in die Cloud verschoben werden, wird der Abbruch von Anforderungen immer wichtiger. Cloudbasierte Anwendungen hängen stärker von externen Diensten ab, die unterschiedlich lange dauern können. Ein Standardmuster ist zum Beispiel, externe Anforderungen mit exponentiellem Backoff zu wiederholen. Wenn Ihre Anwendung von mehreren dieser Dienste abhängt, ist es sinnvoll, eine Obergrenze für die Zeitüberschreitung für die gesamte Anforderungsbearbeitung festzulegen.

Aktueller Status der Async-Unterstützung

Viele Bibliotheken wurden aktualisiert, um mit Async kompatibel zu sein. Async-Unterstützung wurde zu Entity Framework (im EntityFramework NuGet-Paket) in Version 6 hinzugefügt. Sie müssen jedoch aufpassen, dass Sie Lazy Loading beim asynchronen Arbeiten vermeiden, da Lazy Loading immer synchron erfolgt. HttpClient (im Microsoft.Net.Http NuGet-Paket) ist ein moderner HTTP-Client, der für Async entwickelt wurde, und ist ideal für das Aufrufen externer REST-APIs geeignet. Dies ist ein moderner Ersatz für HttpWebRequest und WebClient. Die Microsoft Azure Storage Client Library (im WindowsAzure.Storage NuGet-Paket) fügte Async-Unterstützung in Version 2.1 hinzu.

Neuere Frameworks wie Web-API und SignalR unterstützen Async und Await vollständig. Besonders Web-API hat eine eigene Pipeline für die Async-Unterstützung erstellt: nicht nur Async-Controller, sondern auch Async-Filter und -Handler. Web-API und SignalR integrieren Async wie selbstverständlich: Sie können es einfach nutzen und es funktioniert einfach!

Dies bringt uns zu einer traurigeren Geschichte: Heute unterstützt ASP.NET MVC Async und Await nur teilweise. Die Basisunterstützung ist vorhanden: Async-Controlleraktionen und Abbrüche funktionieren richtig. Die ASP.NET-Website verfügt über ein ausgezeichnetes Lernprogramm zur Verwendung von Async-Controlleraktionen in ASP.NET MVC (bit.ly/1m1LXTx). Dies ist die beste Ressource für die ersten Schritte mit Async auf MVC. Leider unterstützt ASP.NET MVC (derzeit) keine Async-Filter (bit.ly/1oAyHLc) oder untergeordnete Async-Aktionen (bit.ly/1px47RG).

ASP.NET Web Forms ist ein älteres Framework, bietet aber ausreichende Unterstützung für Async und Await. Die beste Ressource für die ersten Schritte ist erneut das Lernprogramm auf der ASP.NET-Website für Async-Web Forms (bit.ly/Ydho7W). Bei Web Forms müssen Sie sich aktiv für die Async-Unterstützung entscheiden. Sie müssen zuerst für Page.Async „true“ festlegen, dann können Sie PageAsyncTask verwenden, um Async-Arbeit bei dieser Seite zu registrieren (alternativ können Sie Async-Void-Ereignishandler verwenden). PageAsyncTask unterstützt auch Abbrüche.

Wenn Sie einen benutzerdefinierten HTTP-Handler oder ein HTTP-Modul haben, unterstützt ASP.NET jetzt auch deren asynchrone Versionen. HTTP-Handler werden über HttpTaskAsyncHandler unterstützt (bit.ly/1nWpWFj), und HTTP-Module werden über EventHandlerTaskAsyncHelper unterstützt (bit.ly/1m1Sn4O).

Zum Zeitpunkt dieses Artikels arbeitet das ASP.NET-Team an einem neuen Projekt namens ASP.NET vNext. In vNext ist die gesamte Pipeline standardmäßig asynchron. Derzeit ist der Plan, MVC und Web-API in einem einzigen Framework zu kombinieren, das Async/Await vollständig unterstützt (einschließlich Async-Filter und Async-Ansichtskomponenten). Andere async-fähige Frameworks wie SignalR werden in vNext ein Zuhause finden. Die Zukunft liegt wahrhaft in Async.

Sicherheitsvorkehrungen respektieren

ASP.NET 4.5 führte einige neue Sicherheitsvorkehrungen ein, die Ihnen bei der Behebung asynchroner Probleme in Ihrer Anwendung helfen. Diese sind standardmäßig aktiviert und sollten aktiv bleiben.

Wenn ein synchroner Handler versucht, asynchrone Arbeit auszuführen, erhalten Sie eine InvalidOperationException mit der Meldung: „Im Moment kann kein asynchroner Vorgang gestartet werden.“ Es gibt zwei primäre Gründe für diese Ausnahme. Der erste Grund ist, wenn eine Web Forms-Seite einen Async-Ereignishandler hat, aber Page.Async nicht auf „true“ gesetzt wird. Der zweite Grund ist, wenn der synchrone Code eine Async-Void-Methode aufruft. Dies ist noch ein zweiter Grund, Async-Void zu vermeiden.

Die andere Sicherheitsvorkehrung ist für asynchrone Handler: Wenn ein asynchroner Handler die Anforderung abschließt, aber ASP.NET erkennt, dass asynchrone Arbeit noch nicht abgeschlossen wurde, erhalten Sie eine Invalid­OperationException mit der Meldung: „Ein asynchrones Modul bzw. ein asynchroner Handler wurde abgeschlossen, während ein asynchroner Vorgang noch ausstand.“ Dies liegt normalerweise an asynchronem Code, der eine Async-Void-Methode aufruft, kann aber auch durch eine inkorrekte Verwendung einer Komponente eines ereignisbasierten asynchronen Musters (Event-based Asynchronous Pattern, EAP) verursacht sein (bit.ly/19VdUWu).

Es gibt eine Option, die Sie zum Deaktivieren beider Sicherheitsvorkehrungen verwenden können: HttpContext.AllowAsyncDuringSyncStages (kann auch in web.config festgelegt werden). Auf einigen Internetseiten wird vorgeschlagen, dies immer einzustellen, wenn Sie diese Ausnahmen sehen. Ich plädiere vehement dagegen. Ich weiß gar nicht, weshalb dies überhaupt möglich ist. Das Deaktivieren der Sicherheitsvorkehrungen ist eine schreckliche Idee. Der einzige Grund besteht meiner Meinung nach darin, dass Ihr Code bereits extrem fortschrittliche asynchrone Tätigkeiten ausführt (mehr als alles, was ich jemals versucht habe), und Sie ein Multithreading-Genie sind. Wenn Sie also diesen gesamten Artikel gähnend gelesen haben und sich gedacht haben, „Hey, ich bin doch kein Neueinsteiger“, dann nehme ich an, dass Sie in Betracht ziehen können, die Sicherheitsvorkehrungen zu deaktivieren. Für den Rest von uns ist dies eine extrem gefährliche Option, die nur festgelegt werden sollte, wenn Sie sich den Auswirkungen vollständig bewusst sind.

Erste Schritte

Und jetzt? Sind Sie bereit, von Async und Await zu profitieren? Ich danke Ihnen für Ihre Geduld.

Sehen Sie sich zuerst noch einmal den Abschnitt „Asynchroner Code ist keine Wunderwaffe“ in diesem Artikel an, um sicherzugehen, dass Async/Await für Ihre Architektur geeignet ist. Aktualisieren Sie Ihre Anwendung als nächstes auf ASP.NET 4.5 und deaktivieren Sie den Quirksmodus (es ist keine schlechte Idee, diesen an diesem Punkt auszuführen, nur um sicherzustellen, dass alles funktioniert). Jetzt können Sie mit der richtigen Async/Await-Arbeit beginnen.

Beginnen Sie mit den „Blättern“. Überlegen Sie, wie Ihre Anforderungen verarbeitet werden, und identifizieren Sie E/A-basierte Vorgänge, besonders alle netzwerkbasierten Vorgänge. Häufige Beispiele sind Datenbankabfragen und -befehle sowie Aufrufe zu anderen Webdiensten und APIs. Wählen Sie einen Vorgang aus, und recherchieren Sie etwas, um die beste Option für die Durchführung dieses Vorgangs mit Async/Await herauszufinden. Viele der integrierten BCL-Typen sind jetzt in .NET Framework 4.5 Async-fähig; SmtpClient hat zum Beispiel die SendMailAsync-Methoden. Für einige Typen gibt es Async-fähige Ersetzungen; HttpWebRequest und WebClient können zum Beispiel mit HttpClient ersetzt werden. Stufen Sie ggf. Ihre Bibliotheksversionen herauf. Entity Framework erhält zum Beispiel in EF6 Async-kompatible Methoden.

Vermeiden Sie jedoch Scheinasynchronie in Bibliotheken. Scheinasynchronie liegt vor, wenn eine Komponente eine Async-fähige API hat, aber nur durch das Wrapping der synchronen API innerhalb eines Threadpoolthreads implementiert wird. Dies ist kontraproduktiv für die Skalierbarkeit auf ASP.NET. Ein prominentes Beispiel für Scheinasynchronie ist Newtonsoft JSON.NET, eine ansonsten ausgezeichnete Bibliothek. Die beste Lösung ist, nicht die (scheinbar) asynchronen Versionen für das Serialisieren von JSON aufzurufen, sondern stattdessen die synchronen Versionen aufzurufen. Ein komplizierteres Beispiel für Scheinasynchronie sind die BCL-Dateiströme. Wenn ein Dateistrom geöffnet wird, muss er explizit für asynchronen Zugriff geöffnet werden. Andernfalls wird dieser Scheinasynchronie verwenden, wodurch synchron ein Threadpoolthread auf den Dateilese- und -schreibvorgängen blockiert wird.

Wenn Sie ein „Blatt“ ausgewählt haben, beginnen Sie mit einer Methode in Ihrem Code, die einen Aufruf in diese API macht, und machen Sie diese zu einer Async-Methode, welche die Async-fähige API über Await aufruft. Wenn die API, die Sie aufrufen, CancellationToken unterstützt, sollte Ihre Methode einen CancellationToken nehmen und ihn zur API-Methode weitergeben.

Wenn Sie eine Methode als asynchron markieren, sollten Sie deren Rückgabetyp ändern: Void wird Task, und ein nicht-Void-Typ T wird Task<T>. Sie werden bemerken, dass alle Aufrufer dieser Methode asynchron werden müssen, sodass sie auf die Aufgabe warten können, usw. Hängen Sie Async an den Namen Ihrer Methode an, um die Konventionen des aufgabenbasierten asynchronen Musters einzuhalten (bit.ly/1uBKGKR).

Erlauben Sie dem Async/Await-Muster, Ihre Aufrufliste bis zum „Trunk“ aufzuziehen. Beim Trunk wird Ihr Code mit dem ASP.NET-Framework (MVC, Web Forms, Web API) verknüpft. Lesen Sie sich das entsprechende Lernprogramm im Abschnitt „Aktueller Status der Async-Unterstützung“ in diesem Artikel durch, um den Async-Code in Ihr Framework zu integrieren.

Identifizieren Sie dabei alle Thread-lokalen Zustände. Da asynchrone Anforderungen Threads ändern können, werden Thread-lokale Zustände wie ThreadStaticAttribute, ThreadLocal<T>, Threaddatenslots und CallContext.GetData/SetData nicht funktionieren. Ersetzen Sie diese wenn möglich mit HttpContext.Items, oder speichern Sie unveränderliche Daten in CallContext.LogicalGetData/LogicalSetData.

Hier ist ein hilfreicher Tipp: Sie können Ihren Code (temporär) duplizieren, um eine vertikale Partition zu erstellen. Mit dieser Technik ändern Sie synchrone Methoden nicht in asynchrone Methoden, sondern kopieren die gesamte synchrone Methode und ändern die Kopie dann in asynchron. Sie können den Großteil Ihrer Anwendung dann mit den synchronen Methoden aufbewahren und nur eine kleines vertikales Stück Asynchronie erstellen. Dies ist perfekt, wenn Sie Async als Machbarkeitsnachweis erkunden oder Auslastungstests auf einem Teil der Anwendung durchführen möchten, um ein Gefühl dafür zu bekommen, wie Ihr System skalieren könnte. Sie können eine Anforderung (oder Seite) haben, die vollständig asynchron ist, während der Rest der Anwendung synchron bleibt. Natürlich möchten Sie keine Duplikate für jede der Methoden aufbewahren. Am Ende wird der gesamte E/A-gebundene Code asynchron sein, und die synchronen Kopien können entfernt werden.

Zusammenfassung

Ich hoffe, dass dieser Artikel Ihnen ein grundlegendes Verständnis asynchroner Anforderungen auf ASP.NET vermittelt hat. Mit Async und Await ist es einfacher denn je, Webanwendungen, -dienste und -APIs zu erstellen, welche die Serverressourcen maximal ausnutzen. Async ist einfach wunderbar!