Asynchrone Seiten in ASP.NET 2.0

Veröffentlicht: 23. Nov 2005

Von Jeff Prosise

ASP.NET 2.0 bietet eine Fülle von neuen Features, die von deklarativer Datenbindung und Masterseiten bis zu Verwaltungsdiensten für Mitgliedschaften und Rollen reichen. Das interessanteste neue Feature sind meiner Meinung nach jedoch asynchrone Seiten.

Laden Sie den Code zu diesem Artikel herunter: WickedCode0510.exe (123 KB)

Wenn ASP.NET eine Anforderung für eine Seite erhält, holt es sich einen Thread aus dem Threadpool und weist diesem die Anforderung zu. Eine normale (synchrone) Seite belegt diesen Thread für die Dauer der Anforderung und verhindert so, dass der Thread andere Anforderungen verarbeitet. Wenn eine synchrone Anforderung E/A-gebunden ist, beispielsweise, wenn sie einen Remote-Webdienst aufruft oder eine Remote-Datenbank abfragt und auf eine Antwort wartet, verharrt der dieser Anforderung zugewiesene Thread in Untätigkeit, bis die Antwort erfolgt. Dies behindert die Skalierbarkeit, da im Threadpool nur eine begrenzte Anzahl von Threads zur Verfügung steht. Wenn alle Threads, die Anforderungen verarbeiten, dadurch blockiert werden, dass sie auf den Abschluss von E/A-Operationen warten, werden zusätzliche Anforderungen in die Warteschlange gestellt, bis ein Thread verfügbar wird. Im besten Fall nimmt der Durchsatz ab, da Anforderungen länger auf die Verarbeitung warten. Im ungünstigsten Fall wird die Warteschlange vollständig gefüllt, nachfolgende Anforderungen schlagen fehl, und ASP.NET gibt die Fehlermeldung 503 ("Server nicht verfügbar") aus.

Asynchrone Seiten bieten eine elegante Lösung für Probleme, die durch E/A-gebundene Anforderungen verursacht werden. Die Verarbeitung einer Seite beginnt mit einem Thread aus dem Threadpool; dieser Thread wird jedoch als Reaktion auf ein Signal von ASP.NET an den Threadpool zurückgegeben, sobald eine asynchrone E/A-Operation startet. Wenn die Operation abgeschlossen ist, holt ASP.NET einen anderen Thread aus dem Threadpool und schließt die Verarbeitung der Anforderung ab. Die Skalierbarkeit nimmt zu, da die Threads aus dem Threadpool effizienter verwendet werden. Threads, die sonst auf den Abschluss einer E/A-Operation warten und dann nicht verfügbar sind, lassen sich nun zur Bearbeitung weiterer Anforderungen verwenden. Direkte Nutznießer sind Anforderungen, die keine länger dauernden E/A-Operationen ausführen, daher schnell in die Pipeline gelangen und diese wieder verlassen können. Lange Wartezeiten bei der Eingliederung in die Pipeline haben einen unverhältnismäßig negativen Einfluss auf die Leistung derartiger Anforderungen.

Die Infrastruktur für asynchrone Seiten in ASP.NET 2.0 Beta 2 ist nicht besonders ausführlich dokumentiert. Lassen Sie uns das Land der asynchronen Seiten erkunden, um diesen Mangel zu beheben. Denken Sie daran, dass diese Kolumne auf der Grundlage von Beta-Versionen von ASP.NET 2.0 und .NET Framework 2.0 entstanden ist.

Auf dieser Seite

Asynchrone Seiten in ASP.NET 1.x
Asynchrone Seiten in ASP.NET 2.0
Asynchrone Datenbindung
Asynchrones Aufrufen von Webdiensten
Asynchrone Aufgaben
Zusammenfassung
Der Autor

Asynchrone Seiten in ASP.NET 1.x

ASP.NET 1.x unterstützt asynchrone Seiten per se nicht; es ist jedoch möglich, diese mit ein wenig Ausdauer und einer Prise Einfallsreichtum dennoch zu erstellen. Einen hervorragenden Überblick dazu bietet der Artikel von Fritz Onion mit dem Titel "Use Threads and Build Asynchronous Handlers in Your Server-Side Web Code" (in englischer Sprache) in der Juni-Ausgabe 2003 von MSDN®Magazine.

Der Trick besteht darin, IHttpAsyncHandler in der Codebehind-Klasse einer Seite zu implementieren und dadurch ASP.NET anzuweisen, Anforderungen nicht durch den Aufruf der Methode IHttpHandler.ProcessRequest der Seite zu verarbeiten, sondern durch den Aufruf von IHttpAsyncHandler.BeginProcessRequest. Die Implementierung von BeginProcessRequest kann dann einen weiteren Thread starten. Dieser Thread ruft base.ProcessRequest auf und startet so den normalen Anforderungsverarbeitungszyklus (vollständig mit Ereignissen wie Load und Render), jedoch mit einem Thread, der nicht aus dem Threadpool stammt. In der Zwischenzeit kehrt BeginProcessRequest unmittelbar nach dem Start des neuen Threads zurück. Dadurch kann der Thread, der BeginProcessRequest ausführt, wieder in den Threadpool aufgenommen werden.

Das ist die grundlegende Idee, der Teufel steckt jedoch im Detail. Unter anderem müssen Sie IAsyncResult implementieren und von BeginProcessRequest zurückgeben lassen. Das bedeutet typischerweise, dass ein ManualResetEvent-Objekt erstellt und diesem signalisiert werden muss, wann ProcessRequest im Hintergrund-Thread beendet wird. Zusätzlich müssen Sie den Thread zur Verfügung stellen, der base.ProcessRequest aufruft. Unglücklicherweise sind die meisten konventionellen Techniken zum Verschieben von Aufgaben in Hintergrundthreads einschließlich Thread.Start, ThreadPool.QueueUserWorkItem und asynchroner Delegationen in ASP.NET-Anwendungen kontraproduktiv, da sie entweder Threads aus dem Threadpool entnehmen oder das Risiko unbegrenzter Threadvermehrung erhöhen. In einer guten Implementierung einer asynchronen Seite wird ein benutzerdefinierter Threadpool verwendet, und benutzerdefinierte Threadpoolklassen zu schreiben, ist nicht gerade trivial (weitere Informationen finden Sie in der Kolumne .NET Matters (in englischer Sprache) in der Februar-Ausgabe 2005 von MSDN Magazine).

Grundsätzlich ist das Erstellen von asynchronen Seiten in ASP.NET 1.x also nicht unmöglich, jedoch langwierig. Nachdem Sie dies ein- bis zweimal gemacht haben, wünschen Sie sich sicherlich eine bessere Möglichkeit dafür. Dazu gibt es heutzutage ASP.NET 2.0.

Asynchrone Seiten in ASP.NET 2.0

ASP.NET 2.0 vereinfacht die Erstellung asynchroner Seiten enorm. Sie beginnen, indem Sie das Attribut Async="true" in die Direktive @ Page der Seite einfügen, etwa so:

<%@ Page Async="true" ... %>

Intern wird ASP.NET dadurch angewiesen, IHttpAsyncHandler in der Seite zu implementieren. Als Nächstes rufen Sie die Methode Page.AddOnPreRenderCompleteAsync in einem frühen Stadium der Seitenausführung auf (z. B. in Page_Load), um eine Begin- und eine End-Methode zu registrieren, wie im folgenden Codeabschnitt dargestellt:

AddOnPreRenderCompleteAsync (
    new BeginEventHandler(MyBeginMethod),
    new EndEventHandler (MyEndMethod)
);

Jetzt folgt der interessante Teil. Die Seite durchläuft ihren normalen Verarbeitungslebenszyklus bis kurz nach dem Auslösen des Ereignisses PreRender. Dann ruft ASP.NET die mithilfe von AddOnPreRenderCompleteAsync registrierte Begin-Methode auf. Die Aufgabe der Begin-Methode besteht darin, einen asynchronen Vorgang wie beispielsweise eine Datenbankabfrage oder einen Aufruf eines Webdiensts zu starten und unmittelbar fortzufahren. Zu diesem Zeitpunkt wird der der Anforderung zugewiesene Thread an den Threadpool zurückgegeben. Die Begin-Methode gibt außerdem ein IAsyncResult zurück, wodurch ASP.NET ermitteln kann, wann der asynchrone Vorgang abgeschlossen wurde. Zu diesem Zeitpunkt entnimmt ASP.NET einen Thread aus dem Threadpool und ruft die End-Methode auf. Nachdem die End-Methode beendet ist, führt ASP.NET den restlichen zum Lebenszyklus der Seite gehörigen Teil aus. Dazu gehört auch die Darstellungsphase. In der Zeit zwischen dem Ende der Begin-Methode und dem Aufruf der End-Methode steht der Thread, der die Anforderung verarbeitet, zur Verarbeitung anderer Anforderungen zur Verfügung. Die Seitendarstellung wird bis zum Aufruf der End-Methode verzögert. Da die Version 2.0 des .NET Framework eine ganze Reihe von Möglichkeiten zur Ausführung von asynchronen Vorgängen zur Verfügung stellt, brauchen Sie teilweise nicht einmal IAsyncResult zu implementieren. Stattdessen erledigt das Framework die Implementierung für Sie.

Die Codebehind-Klasse im Code-Beispiel 1 stellt ein Beispiel hierfür dar. Die zugehörige Seite enthält ein Steuerelement Bezeichnungsfeld mit der ID "Output". Die Seite verwendet die Klasse System.Net.HttpWebRequest, um den Inhalt von https://msdn.microsoft.com anzufordern. Anschließend analysiert sie den zurückgegebenen HTML-Code und gibt alle gefundenen HREF-Ziele im Steuerelement Bezeichnungsfeld aus.

Code-Beispiel 1

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Net;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

public partial class AsyncPage : System.Web.UI.Page
{
    private WebRequest _request;

    void Page_Load (object sender, EventArgs e)
    {
        AddOnPreRenderCompleteAsync (
            new BeginEventHandler(BeginAsyncOperation),
            new EndEventHandler (EndAsyncOperation)
        );
    }

    IAsyncResult BeginAsyncOperation (object sender, EventArgs e, 
        AsyncCallback cb, object state)
    {
        _request = WebRequest.Create("https://msdn.microsoft.com");
        return _request.BeginGetResponse (cb, state);
    }
    void EndAsyncOperation (IAsyncResult ar)
    {
        string text;
        using (WebResponse response = _request.EndGetResponse(ar))
        {
            using (StreamReader reader = 
                new StreamReader(response.GetResponseStream()))
            {
                text = reader.ReadToEnd();
            }
        }

        Regex regex = new Regex ("href\\s*=\\s*\"([^\"]*)\"", 
            RegexOptions.IgnoreCase);
        MatchCollection matches = regex.Matches(text);

        StringBuilder builder = new StringBuilder(1024);
        foreach (Match match in matches)
        {
            builder.Append (match.Groups[1]);
            builder.Append("<br/>");
        }

        Output.Text = builder.ToString ();
    }
}

Da es lange dauern kann, bis eine Antwort auf eine HTTP-Anforderung zurückgegeben wird, wird diese von AsyncPage.aspx.cs asynchron verarbeitet. Die Begin- und die End-Methode werden in Page_Load registriert, und in der Begin-Methode wird HttpWebRequest.BeginGetResponse aufgerufen, um eine asynchrone HTTP-Anforderung zu starten. BeginAsyncOperation gibt das von BeginGetResponse zurückgegebene IAsyncResult an ASP.NET zurück. Dies führt dazu, dass ASP.NET EndAsyncOperation aufruft, sobald die HTTP-Anforderung abgeschlossen ist. EndAsyncOperation analysiert wiederum den Inhalt und schreibt die Ergebnisse in das Steuerelement Bezeichnungsfeld. Anschließend wird die Seite wiedergegeben und eine HTTP-Antwort an den Browser gesendet.

Synchrone und asynchrone Seitenverarbeitung
Abbildung 1: Synchrone und asynchrone Seitenverarbeitung

Abbildung 2 veranschaulicht den Unterschied zwischen einer synchronen und einer asynchronen Seite in ASP.NET 2.0. Wenn eine synchrone Seite angefordert wird, weist ASP.NET die Anforderung einem Thread aus dem Threadpool zu und führt die Seite innerhalb dieses Threads aus. Wenn die Anforderung zur Ausführung einer E/A-Operation anhält, ist der Thread so lange gebunden, bis die Operation beendet ist und der Lebenszyklus der Seite abgeschlossen werden kann. Eine asynchrone Seite wird im Gegensatz dazu bis zum Ereignis PreRender normal ausgeführt. Dann wird die Begin-Methode, die mithilfe von AddOnPreRenderCompleteAsync registriert wurde, aufgerufen. Nach diesem Aufruf wird der die Anforderung verarbeitende Thread wieder an den Threadpool zurückgegeben. Die Begin-Methode startet eine asynchrone E/A-Operation, und ASP.NET entnimmt nach Abschluss der Operation einen anderen Thread aus dem Threadpool, ruft die End-Methode auf und vollendet mit diesem Thread den verbleibenden Lebenszyklus der Seite.

Die Ausgabe des Programms Trace zeigt den Zeitpunkt, an dem die asynchrone Verarbeitung einer asynchronen Seite einsetzt
Abbildung 2: Die Ausgabe des Programms Trace zeigt den Zeitpunkt, an dem die asynchrone Verarbeitung einer asynchronen Seite einsetzt

Der Aufruf der Begin-Methode markiert den Zeitpunkt, an dem die asynchrone Verarbeitung einsetzt. Die Ausgabe von Trace in Abbildung 2 zeigt genau, an welcher Stelle die asynchrone Verarbeitung einsetzt. Wenn AddOnPreRenderCompleteAsync aufgerufen werden soll, muss dies erfolgen, bevor die asynchrone Verarbeitung einsetzt, d. h. nicht nach dem Ereignis PreRender.

Asynchrone Datenbindung

Für ASP.NET-Seiten ist die direkte Verwendung von HttpWebRequest für die Anforderung anderer Seiten nicht gerade üblich, es ist jedoch üblich, Datenbanken abzufragen und die Ergebnisse für die Datenbindung zu verwenden. Wie würden Sie also asynchrone Seiten verwenden, um eine asynchrone Datenbindung zu erreichen? Die Codebehind-Klasse, im Code-Beispiel 2, stellt eine Möglichkeit dar.

Code-Beispiel 2

using System;
using System.Data;
using System.Data.SqlClient;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Configuration;

public partial class AsyncDataBind : System.Web.UI.Page
{
    private SqlConnection _connection;
    private SqlCommand _command;
    private SqlDataReader _reader;


    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            // Hook PreRenderComplete event for data binding
            this.PreRenderComplete += 
                new EventHandler(Page_PreRenderComplete);

            // Register async methods
            AddOnPreRenderCompleteAsync(
                new BeginEventHandler(BeginAsyncOperation),
                new EndEventHandler(EndAsyncOperation)
            );
        }
    }
    IAsyncResult BeginAsyncOperation (object sender, EventArgs e, 
        AsyncCallback cb, object state)
    {
        string connect = WebConfigurationManager.ConnectionStrings
            ["PubsConnectionString"].ConnectionString;
        _connection = new SqlConnection(connect);
        _connection.Open();
        _command = new SqlCommand(
            "SELECT title_id, title, price FROM titles", _connection);
        return _command.BeginExecuteReader (cb, state);
    }

    void EndAsyncOperation(IAsyncResult ar)
    {
        _reader = _command.EndExecuteReader(ar);
    }

    protected void Page_PreRenderComplete(object sender, EventArgs e)
    {
        Output.DataSource = _reader;
        Output.DataBind();
    }

    public override void Dispose()
    {
        if (_connection != null) _connection.Close();
        base.Dispose();
    }
}

AsyncDataBind.aspx.cs verwendet dasselbe AddOnPreRenderCompleteAsync-Muster wie AsyncPage.aspx.cs. Doch anstatt HttpWebRequest.BeginGetResponse ruft die Methode BeginAsyncOperation den SqlCommand.BeginExecuteReader (neu in ADO.NET 2.0) auf, um eine asynchrone Datenbankabfrage auszuführen. Wenn der Aufruf abgeschlossen ist, ruft EndAsyncOperation die Methode SqlCommand.EndExecuteReader auf, um einen SqlDataReader zu erhalten, der anschließend in einem privaten Feld gespeichert wird. Innerhalb eines Ereignishandlers für das Ereignis PreRenderComplete, das nach Abschluss der asynchronen Operation, jedoch vor der Wiedergabe der Seite ausgelöst wird, bindet die Methode den SqlDataReader dann an das Ausgabesteuerelement GridView. Nach außen sieht die Seite wie eine normale (synchrone) Seite aus, die ein GridView verwendet, um die Ergebnisse einer Datenbankabfrage anzuzeigen. Intern betrachtet ist diese Seite wesentlich skalierbarer, da sie keinen Thread aus dem Threadpool bindet, während sie auf das Ergebnis der Abfrage wartet.

Asynchrones Aufrufen von Webdiensten

Eine weitere E/A-bezogene Aufgabe, die üblicherweise von ASP.NET-Webseiten ausgeführt wird, besteht in Aufrufen von Webdiensten. Da es lange dauern kann, bis eine Antwort auf Webdienstaufrufe erfolgt, sind Seiten, die derartige Aufrufe ausführen, ideale Kandidaten für die asynchrone Verarbeitung.

Code-Beispiel 3 zeigt eine Möglichkeit, eine asynchrone Seite zu erstellen, die einen Webdienst aufruft. Sie verwendet den gleichen AddOnPreRenderCompleteAsync-Mechanismus wie in Code-Beispiel 2 und Code-Beispiel 2. Die Begin-Methode der Seite startet einen asynchronen Webdienstaufruf, indem sie die asynchrone Begin-Methode des Webdienstproxys aufruft. Die End-Methode speichert eine von der Webmethode zurückgegebene Referenz auf das DataSet in einem privaten Feld, und der PreRenderComplete-Handler bindet das DataSet an ein GridView. Zur Referenz wird die aufgerufene Webmethode in Codeabschnitt nach Code-Beispiel 3 dargestellt:

Code-Beispiel 3

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class AsyncWSInvoke1 : System.Web.UI.Page
{
private WS.PubsWebService _ws;
private DataSet _ds;

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        // Hook PreRenderComplete event for data binding
        this.PreRenderComplete += 
            new EventHandler(Page_PreRenderComplete);

        // Register async methods
        AddOnPreRenderCompleteAsync(
            new BeginEventHandler(BeginAsyncOperation),
            new EndEventHandler(EndAsyncOperation)
        );
    }
}

IAsyncResult BeginAsyncOperation (object sender, EventArgs e, 
    AsyncCallback cb, object state)
{
    _ws = new WS.PubsWebService();
    // Fix up URL for call to local VWD-hosted Web service
    _ws.Url = new Uri(Request.Url, "Pubs.asmx").ToString();
    _ws.UseDefaultCredentials = true;
    return _ws.BeginGetTitles (cb, state);
}

void EndAsyncOperation(IAsyncResult ar)
{
    _ds = _ws.EndGetTitles(ar);
}

protected void Page_PreRenderComplete(object sender, EventArgs e)
{
    Output.DataSource = _ds;
    Output.DataBind();
}

public override void Dispose()
{
    if (_ws != null) _ws.Dispose();
    base.Dispose();
}
}

[WebMethod]
public DataSet GetTitles ()
{
    string connect = WebConfigurationManager.ConnectionStrings
        ["PubsConnectionString"].ConnectionString;
    SqlDataAdapter adapter = new SqlDataAdapter
        ("SELECT title_id, title, price FROM titles", connect);
    DataSet ds = new DataSet();
    adapter.Fill(ds);
    return ds;
}

Das ist ein Weg, jedoch nicht der einzige. Die .NET Framework 2.0-Webdienstproxys unterstützen zwei Mechanismen zum Absetzen von asynchronen Aufrufen an Webdienste. Der eine Mechanismus besteht in Begin- und End-Methoden pro Methode, der von .NET Framework 1.x- und 2.0-Webdienstproxys unterstützt wird. Der andere besteht in den neuen MethodAsync-Methoden und MethodCompleted-Ereignissen, die nur bei den Webdienstproxys von .NET Framework 2.0 zu finden sind.

Wenn ein Webdienst über eine Methode namens Foo verfügt, enthält ein Webdienstproxy von .NET Framework 2.0 zusätzlich zu den Methoden Foo, BeginFoo und EndFoo eine Methode namens FooAsync und ein Ereignis namens FooCompleted. Sie können Foo asynchron aufrufen, indem Sie einen Handler für FooCompleted-Ereignisse registrieren und FooAsync, wie im Folgenden dargestellt, aufrufen:

proxy.FooCompleted += new FooCompletedEventHandler (OnFooCompleted);
proxy.FooAsync (...);
...
void OnFooCompleted (Object source, FooCompletedEventArgs e)
{
    // Called when Foo completes
}

Wird der asynchrone Aufruf, der von FooAsync gestartet wurde, abgeschlossen, wird ein FooCompleted-Ereignis ausgelöst, das den FooCompleted-Ereignishandler aufruft. Sowohl der Delegat, der als Wrapper für den Ereignishandler (FooCompletedEventHandler) dient, als auch der zweite an ihn übergebene Parameter (FooCompletedEventArgs) werden zusammen mit dem Webdienstproxy erzeugt. Sie können über FooCompletedEventArgs.Result auf den Rückgabewert von Foo zugreifen.

In Code-Beispiel 4 wird eine Codebehind-Klasse dargestellt, die die Methode GetTitles eines Webdiensts mithilfe des MethodAsync-Musters asynchron aufruft. Diese Seite ist funktionell identisch mit der Seite im Code-Beispiel 3. Intern ist sie jedoch ganz anders. AsyncWSInvoke2.aspx schließt die Direktive @ Page Async="true" ein, genau wie AsyncWSInvoke1.aspx. Die Klasse AsyncWSInvoke2.aspx.cs ruft jedoch nicht AddOnPreRenderCompleteAsync auf, sondern registriert einen Handler für GetTitlesCompleted-Ereignisse und ruft GetTitlesAsync vom Webdienstproxy auf. ASP.NET verzögert weiterhin die Darstellung der Seite, bis GetTitlesAsync abgeschlossen ist. Intern wird eine Instanz von System.Threading.SynchronizationContext verwendet, eine weitere neue Klasse in ASP.NET 2.0, um Benachrichtigungen zu erhalten, wann der asynchrone Aufruf startet und abgeschlossen wird.

Code-Beispiel 4

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

public partial class AsyncWSInvoke2 : System.Web.UI.Page
{
    private WS.PubsWebService _ws;
    private DataSet _ds;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            // Hook PreRenderComplete event for data binding
            this.PreRenderComplete += 
                new EventHandler(Page_PreRenderComplete);

            // Call the Web service asynchronously
            _ws = new WS.PubsWebService();
            _ws.GetTitlesCompleted += new 
                WS.GetTitlesCompletedEventHandler(GetTitlesCompleted);
            _ws.Url = new Uri(Request.Url, "Pubs.asmx").ToString();
            _ws.UseDefaultCredentials = true;
            _ws.GetTitlesAsync();
        }
    }

    void GetTitlesCompleted(Object source, 
        WS.GetTitlesCompletedEventArgs e)
    {
        _ds = e.Result;
    }

    protected void Page_PreRenderComplete(object sender, EventArgs e)
    {
        Output.DataSource = _ds;
        Output.DataBind();
    }

    public override void Dispose()
    {
        if (_ws != null) _ws.Dispose();
        base.Dispose();
    }
}

Es gibt zwei Vorteile bei der Verwendung von MethodAsync anstelle von AddOnPreRenderCompleteAsync zur Implementierung von asynchronen Seiten. Erstens leitet MethodAsync Identitätswechsel, Kultur und HttpContext.Current an den Ereignishandler MethodCompleted weiter. AddOnPreRenderCompleteAsync tut dies nicht. Wenn die Seite mehrere asynchrone Aufrufe ausführt und die Darstellung bis zum Abschluss aller Aufrufe verzögern muss, müssen Sie zweitens bei der Verwendung von AddOnPreRenderCompleteAsync ein IAsyncResult erstellen, das nicht signalisiert wird, bis alle Aufrufe abgeschlossen sind. Mit MethodAsync sind solche Umwege nicht nötig. Sie führen einfach beliebig viele Aufrufe aus, und das ASP.NET-Modul verzögert die Darstellungsphase bis der letzte Aufruf abgeschlossen ist.

Asynchrone Aufgaben

MethodAsync stellt eine bequeme Möglichkeit dar, mehrere asynchrone Webdienstaufrufe von einer asynchronen Seite aus auszuführen und die Darstellungsphase bis zum Abschluss aller Aufrufe zu verzögern. Doch wie gehen Sie vor, wenn Sie mehrere asynchrone E/A-Operationen in einer asynchronen Seite ausführen und dabei keine Webdienste nutzen möchten? Bedeutet dies, dass Sie wieder ein IAsyncResult erstellen müssen, das Sie an ASP.NET zurückgeben können, damit es weiß, wann der letzte Aufruf abgeschlossen wurde? Glücklicherweise nicht.

ASP.NET 2.0 führt mit der Klasse System.Web.UI.Page eine weitere Methode ein, um asynchrone Operationen zu erleichtern: RegisterAsyncTask. RegisterAsyncTask bietet gegenüber AddOnPreRenderCompleteAsync vier Vorteile. Erstens können Sie mit RegisterAsyncTask zusätzlich zu den Begin- und End-Methoden eine Timeout-Methode registrieren, die aufgerufen wird, wenn der Abschluss einer asynchronen Operation zu lange dauert. Sie können den Timeoutwert deklarativ festlegen, indem Sie ein AsyncTimeout-Attribut in die @ Page-Direktive der Seite einfügen. AsyncTimeout="5" legt den Timeoutwert auf 5 Sekunden fest. Der zweite Vorteil besteht darin, dass Sie RegisterAsyncTask mehrmals innerhalb einer Anforderung aufrufen können, um mehrere asynchrone Operationen zu registrieren. Wie bei MethodAsync verzögert ASP.NET die Darstellung der Seite bis sämtliche Operationen abgeschlossen sind. Drittens können Sie den vierten Parameter von RegisterAsyncTask verwenden, um den Zustand an die Begin-Methoden zu übergeben. Und schließlich leitet RegisterAsyncTask Identitätswechsel, Kultur und HttpContext.Current an die End- und Timeout-Methoden weiter. Wie bereits oben erwähnt, gilt das Gleiche nicht für eine End-Methode, die mit AddOnPreRenderCompleteAsync registriert wurde.

In sonstiger Hinsicht ist eine asynchrone Seite, die RegisterAsyncTask einsetzt, einer Seite, die AddOnPreRenderCompleteAsync einsetzt, ähnlich. Sie benötigt weiterhin das Attribut Async="true" in der @ Page-Direktive (oder das programmtechnische Äquivalent, das darin besteht, der Seiteneigenschaft AsyncMode den Wert TRUE zuzuweisen), und sie wird weiterhin bis zum Ereignis PreRender normal ausgeführt. Zu diesem Zeitpunkt werden dann die mithilfe von RegisterAsyncTask registrierten Begin-Methoden aufgerufen und die weitere Verarbeitung der Anforderung angehalten, bis die letzte Operation abgeschlossen ist. Um dies zu verdeutlichen, ist die Codebehind-Klasse im Code-Beispiel 5 funktionell gleichwertig mit der Klasse im Code-Beispiel 1, sie verwendet jedoch RegisterTaskAsync anstelle von AddOnPreRenderCompleteAsync. Beachten Sie den Timeouthandler TimeoutAsyncOperation, der aufgerufen wird, wenn der Abschluss von HttpWebRequest.BeginGetRequest zu lange dauert. Die entsprechende ASPX-Datei enthält ein AsyncTimeout-Attribut, das das Timeoutintervall auf 5 Sekunden festlegt. Beachten Sie auch den Nullwert, der im vierten Parameter von RegisterAsyncTask übergeben wird, der auch verwendet werden könnte, um Daten an die Begin-Methode zu übergeben.

Code-Beispiel 5

using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Net;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

public partial class AsyncPageTask : System.Web.UI.Page
{
    private WebRequest _request;

    protected void Page_Load(object sender, EventArgs e)
    {
        PageAsyncTask task = new PageAsyncTask(
            new BeginEventHandler(BeginAsyncOperation),
            new EndEventHandler(EndAsyncOperation),
            new EndEventHandler(TimeoutAsyncOperation),
            null
        );
        RegisterAsyncTask(task);
    }

    IAsyncResult BeginAsyncOperation(object sender, EventArgs e, 
        AsyncCallback cb, object state)
    {
        _request = WebRequest.Create("https://msdn.microsoft.com");
        return _request.BeginGetResponse(cb, state);
    }

    void EndAsyncOperation(IAsyncResult ar)
    {
        string text;
        using (WebResponse response = _request.EndGetResponse(ar))
        {
            using (StreamReader reader = 
                new StreamReader(response.GetResponseStream()))
            {
                text = reader.ReadToEnd();
            }
        }

        Regex regex = new Regex("href\\s*=\\s*\"([^\"]*)\"", 
            RegexOptions.IgnoreCase);
        MatchCollection matches = regex.Matches(text);

        StringBuilder builder = new StringBuilder(1024);
        foreach (Match match in matches)
        {
            builder.Append(match.Groups[1]);
            builder.Append("<br/>");
        }

        Output.Text = builder.ToString();
    }

    void TimeoutAsyncOperation(IAsyncResult ar)
    {
        Output.Text = "Data temporarily unavailable";
    }
}

Der wichtigste Vorteil von RegisterAsyncTask: Asynchrone Seiten können dadurch mehrere asynchrone Aufrufe ausführen und die Darstellung verzögern, bis alle Aufrufe abgeschlossen sind. Dies funktioniert auch für nur einen asynchronen Aufruf perfekt, und es steht eine Timeoutoption zur Verfügung, die AddOnPreRenderCompleteAsync nicht bietet. Wenn Sie eine asynchrone Seite erstellen, die lediglich einen asynchronen Aufruf ausführt, können Sie sowohl AddOnPreRenderCompleteAsync als auch RegisterAsyncTask verwenden. Bei asynchronen Seiten, die zwei oder mehr asynchrone Aufrufe ausführen, erleichtert Ihnen RegisterAsyncTask das Leben jedoch beträchtlich.

Da der Timeoutwert eine Einstellung ist, die für die gesamte Seite statt für jeden Aufruf gilt, fragen Sie sich vielleicht, ob es möglich ist, den Timeoutwert für einzelne Aufrufe unterschiedlich festzulegen. Kurz gesagt: nein. Sie können zwar den Timeoutwert von einer Anforderung zur nächsten variieren, indem Sie die Seiteneigenschaft AsyncTimeout programmtechnisch ändern, Sie können jedoch nicht verschiedene Timeoutwerte verschiedenen Aufrufen zuweisen, die von derselben Anforderung initiiert werden.

Zusammenfassung

Dies ist also der Stand der Dinge zu asynchronen Seiten in ASP.NET 2.0. Sie lassen sich dort bedeutend einfacher implementieren, und die Architektur ermöglicht die Batchverarbeitung mehrerer asynchroner E/A-Operationen in einer Anforderung sowie die Verzögerung der Darstellung einer Seite, bis alle Operationen abgeschlossen sind. Zusammen mit asynchronen ADO.NET-Funktionen und anderen neuen asynchronen Features im .NET Framework bieten asynchrone ASP.NET-Seiten eine leistungsfähige und bequeme Lösung für E/A-gebundene Anforderungen, die die Skalierbarkeit durch die Auslastung des Threadpools behindern.

Ein letzter Punkt, den Sie beim Erstellen von asynchronen Seiten beachten sollten, ist, dass Sie keine asynchronen Operationen starten sollten, die denselben Threadpool nutzen, den auch ASP.NET verwendet. Der Aufruf von ThreadPool.QueueUserWorkItem zu dem Zeitpunkt, an dem die asynchrone Verarbeitung einsetzt, ist kontraproduktiv, da bei diesem Verfahren Threads aus dem Threadpool entnommen werden. Dies führt dazu, dass kein Zugewinn an Threads für die Verarbeitung von Anforderungen zu verzeichnen ist. Im Gegensatz dazu wird der Aufruf von asynchronen Methoden, die im Framework integriert sind, wie beispielsweise HttpWebRequest.BeginGetResponse und SqlCommand.BeginExecuteReader, im Allgemeinen als sicher angesehen, da diese Methoden dazu neigen, Komplettierungsports zur Implementierung asynchronen Verhaltens zu verwenden.

Senden Sie Fragen und Kommentare für Jeff an wicked@microsoft.com (in englischer Sprache).

Der Autor

Jeff Prosise arbeitet als Autor für MSDN Magazine und ist der Verfasser mehrerer Bücher, einschließlich Programming Microsoft .NET (Microsoft Press, 2002). Er ist auch Mitgründer von Wintellect (in englischer Sprache), eine Firma für Softwareberatung und -schulung.

Aus der Ausgabe Oktober 2005 (in englischer Sprache) des MSDN Magazine (in englischer Sprache).