Używanie metod asynchronicznych w programie ASP.NET 4.5

Autor: Rick Anderson

Ten samouczek zawiera informacje na temat tworzenia asynchronicznej aplikacji ASP.NET Web Forms przy użyciu programu Visual Studio Express 2012 for Web, która jest bezpłatną wersją programu Microsoft Visual Studio. Możesz również użyć programu Visual Studio 2012. Poniższe sekcje znajdują się w tym samouczku.

Kompletny przykład jest udostępniany na potrzeby tego samouczka pod adresem
https://github.com/RickAndMSFT/Async-ASP.NET/ na stronie GitHub.

ASP.NET 4.5 Web Pages w połączeniu z .NET 4.5 umożliwia rejestrowanie metod asynchronicznych, które zwracają obiekt typu Task. Program .NET Framework 4 wprowadził asynchroniczną koncepcję programowania, nazywaną zadaniem , a ASP.NET 4.5 obsługuje zadanie. Zadania są reprezentowane przez typ zadania i powiązane typy w przestrzeni nazw System.Threading.Tasks . Program .NET Framework 4.5 opiera się na tej asynchronicznej obsłudze słów kluczowych await i asynchronicznych , które sprawiają, że praca z obiektami zadań jest znacznie mniej złożona niż poprzednie metody asynchroniczne. Słowo kluczowe await to skrót składniowy wskazujący, że fragment kodu powinien asynchronicznie czekać na inny fragment kodu. Słowo kluczowe asynchroniczne reprezentuje wskazówkę, której można użyć do oznaczania metod jako metod asynchronicznych opartych na zadaniach. Kombinacja funkcji await, async i obiektu Task znacznie ułatwia pisanie kodu asynchronicznego na platformie .NET 4.5. Nowy model metod asynchronicznych jest nazywany asynchronicznym wzorcem asynchronicznym opartym na zadaniach (TAP). W tym samouczku założono, że masz pewną znajomość asynchronicznego programowania z użyciem słów kluczowych await i async oraz przestrzeni nazw Task.

Aby uzyskać więcej informacji na temat używania słów kluczowych await i async oraz przestrzeni nazw Task, zobacz następujące odwołania.

Jak przetwarzane są żądania przez pulę wątków

Na serwerze internetowym platforma .NET Framework obsługuje pulę wątków używanych do obsługi żądań ASP.NET. Po nadejściu żądania zostanie wysłany wątek z puli w celu przetworzenia tego żądania. Jeśli żądanie jest przetwarzane synchronicznie, wątek, który przetwarza żądanie, jest zajęty podczas przetwarzania żądania, a ten wątek nie może obsłużyć innego żądania.

Może to nie być problem, ponieważ pula wątków może być wystarczająco duża, aby pomieścić wiele zajętych wątków. Jednak liczba wątków w puli wątków jest ograniczona (domyślna wartość maksymalna dla platformy .NET 4.5 to 5000). W dużych aplikacjach z wysoką współbieżnością długotrwałych żądań wszystkie dostępne wątki mogą być zajęte. Ten warunek jest nazywany głodem wątku. Po osiągnięciu tego warunku, serwer WWW kolejkuje żądania. Jeśli kolejka żądań stanie się pełna, serwer internetowy odrzuca żądania ze stanem HTTP 503 (Serwer jest zbyt zajęty). Pula wątków CLR ma ograniczenia dotyczące dodawania nowych wątków. Jeśli współbieżność jest pęknięta (oznacza to, że witryna internetowa może nagle uzyskać dużą liczbę żądań), a wszystkie dostępne wątki żądań są zajęte z powodu wywołań zaplecza z dużym opóźnieniem, ograniczona szybkość wstrzykiwania wątków może sprawić, że aplikacja będzie reagować bardzo źle. Ponadto każdy nowy wątek dodany do puli wątków wiąże się z dodatkowymi kosztami (np. 1 MB pamięci stosu). Aplikacja internetowa używająca synchronicznych metod do obsługi wywołań o dużym opóźnieniu, gdzie pula wątków rośnie do domyślnej maksymalnej wielkości 5 000 wątków w platformie .NET 4.5, zużywałaby około 5 GB więcej pamięci niż aplikacja, która może obsługiwać te same żądania przy użyciu asynchronicznych metod i tylko 50 wątków. Podczas pracy asynchronicznej nie zawsze korzystasz z wątku. Na przykład, gdy wykonasz asynchroniczne żądanie usługi sieciowej, ASP.NET nie będzie używać żadnych wątków między wywołaniem metody async a await. Użycie puli wątków do obsługi żądań z dużym opóźnieniem może prowadzić do dużego zużycia pamięci i słabego wykorzystania sprzętu serwera.

Przetwarzanie żądań asynchronicznych

W aplikacjach internetowych, które widzą dużą liczbę równoczesnych żądań podczas uruchamiania lub mają zwiększone obciążenie (w przypadku nagłego wzrostu współbieżności), asynchroniczne wywołania usług internetowych zwiększają czas reakcji aplikacji. Żądanie asynchroniczne trwa tyle samo czasu na przetworzenie, co żądanie synchroniczne. Na przykład jeśli żądanie wykonuje wywołanie usługi internetowej, która zajmuje dwie sekundy, żądanie trwa dwie sekundy, niezależnie od tego, czy jest realizowane synchronicznie, czy asynchronicznie. Jednak podczas wywołania asynchronicznego wątek nie jest blokowany i może odpowiadać na inne żądania, czekając na ukończenie pierwszego żądania. W związku z tym żądania asynchroniczne uniemożliwiają kolejkowanie żądań i wzrost puli wątków, gdy istnieje wiele współbieżnych żądań, które wywołują długotrwałe operacje.

Wybieranie metod synchronicznych lub asynchronicznych

W tej sekcji wymieniono wskazówki dotyczące używania metod synchronicznych lub asynchronicznych. Są to tylko wytyczne; zbadaj poszczególne aplikacje indywidualnie, aby określić, czy metody asynchroniczne pomagają w wydajności.

Ogólnie rzecz biorąc, użyj metod synchronicznych dla następujących warunków:

  • Operacje są proste lub krótkie.
  • Prostota jest ważniejsza niż wydajność.
  • Operacje to przede wszystkim operacje procesora CPU, a nie operacje obejmujące duże obciążenie dysku lub sieci. Korzystanie z metod asynchronicznych w operacjach związanych z procesorem CPU nie zapewnia żadnych korzyści i powoduje większe obciążenie.

Ogólnie rzecz biorąc, użyj metod asynchronicznych dla następujących warunków:

  • Wywołujesz usługi, które mogą być używane za pomocą metod asynchronicznych i używasz platformy .NET 4.5 lub nowszej.

  • Operacje są powiązane z siecią lub wejściem/wyjściem, zamiast z procesorem.

  • Równoległość jest ważniejsza niż prostota kodu.

  • Chcesz udostępnić mechanizm, który umożliwia użytkownikom anulowanie długotrwałego żądania.

  • Gdy korzyść z przełączania wątków przewyższa koszt przełącznika kontekstu. Ogólnie rzecz biorąc, należy utworzyć metodę asynchroniczną, jeśli metoda synchroniczna blokuje wątek żądania ASP.NET, nie wykonując przy tym żadnej pracy. Wykonując wywołanie asynchroniczne, wątek żądania ASP.NET nie jest blokowany, nie wykonuje żadnej pracy podczas oczekiwania na ukończenie żądania usługi sieci Web.

  • Testowanie pokazuje, że operacje blokujące są wąskim gardłem w wydajności strony i że IIS może obsługiwać więcej żądań, wykorzystując metody asynchroniczne do obsługi tych wywołań blokujących.

    W przykładzie do pobrania pokazano, jak efektywnie używać metod asynchronicznych. Udostępniony przykład został zaprojektowany w celu zapewnienia prostego pokazu programowania asynchronicznego w ASP.NET 4.5. Przykład nie jest przeznaczony do programowania asynchronicznego w ASP.NET. Przykładowy program wywołuje metody internetowego interfejsu API ASP.NET , które z kolei wywołają metodę Task.Delay w celu symulowania długotrwałych wywołań usług internetowych. Większość aplikacji produkcyjnych nie pokaże tak oczywistych korzyści z używania metod asynchronicznych.

Niewiele aplikacji wymaga, aby wszystkie metody są asynchroniczne. Często konwertowanie kilku metod synchronicznych na metody asynchroniczne zapewnia najlepszy wzrost wydajności wymaganej pracy.

Przykładowa aplikacja

Przykładową aplikację można pobrać z https://github.com/RickAndMSFT/Async-ASP.NET witryny GitHub . Repozytorium składa się z trzech projektów:

  • WebAppAsync: projekt ASP.NET Web Forms korzystający z usługi WebAPIpwg. Większość kodu tego samouczka pochodzi z tego projektu.
  • WebAPIpgw: projekt internetowego interfejsu Products, Gizmos and Widgets API platformy ASP.NET MVC 4, który implementuje kontrolery. Udostępnia ona dane dla projektu WebAppAsync i projektu Mvc4Async .
  • Mvc4Async: projekt ASP.NET MVC 4 zawierający kod używany w innym samouczku. Wykonuje wywołania interfejsu API do usługi WebAPIpwg.

Strona synchroniczna Gizmos

Poniższy kod przedstawia metodę Page_Load synchroniczną używaną do wyświetlania listy gizmos. (W tym artykule gizmo jest fikcyjnym urządzeniem mechanicznym).

public partial class Gizmos : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var gizmoService = new GizmoService();
        GizmoGridView.DataSource = gizmoService.GetGizmos();
        GizmoGridView.DataBind();
    }
}

Poniższy kod przedstawia metodę GetGizmos usługi gizmo.

public class GizmoService
{
    public async Task<List<Gizmo>> GetGizmosAsync(
        // Implementation removed.
       
    public List<Gizmo> GetGizmos()
    {
        var uri = Util.getServiceUri("Gizmos");
        using (WebClient webClient = new WebClient())
        {
            return JsonConvert.DeserializeObject<List<Gizmo>>(
                webClient.DownloadString(uri)
            );
        }
    }
}

Metoda GizmoService GetGizmos przekazuje identyfikator URI do usługi HTTP interfejsu API sieci Web ASP.NET, która zwraca listę danych gizmos. Projekt WebAPIpgw zawiera implementację internetowego interfejsu API gizmos, widget i product kontrolerów.
Na poniższej ilustracji przedstawiono stronę gizmos z przykładowego projektu.

Zrzut ekranu przedstawiający stronę przeglądarki internetowej Sync Gizmos z tabelą gizmos z odpowiednimi szczegółami wprowadzonymi do kontrolerów internetowego interfejsu API.

Tworzenie asynchronicznej strony Gizmos

W przykładzie użyto nowych słów kluczowych asynchronicznych i await (dostępnych w programach .NET 4.5 i Visual Studio 2012), aby umożliwić kompilatorowi zachowanie skomplikowanych przekształceń niezbędnych do programowania asynchronicznego. Kompilator umożliwia pisanie kodu przy użyciu synchronicznych konstrukcji przepływu sterowania języka C#, a kompilator automatycznie stosuje przekształcenia niezbędne do używania wywołań zwrotnych w celu uniknięcia blokowania wątków.

ASP.NET strony asynchroniczne muszą zawierać dyrektywę Page z atrybutem ustawionym Async na wartość "true". Poniższy kod przedstawia dyrektywę Page z atrybutem ustawionym Async na wartość "true" dla strony GizmosAsync.aspx .

<%@ Page Async="true"  Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosAsync.aspx.cs" Inherits="WebAppAsync.GizmosAsync" %>

Poniższy kod przedstawia metodę Gizmos synchroniczną Page_Load i GizmosAsync stronę asynchroniczną. Jeśli przeglądarka obsługuje element znacznika< HTML 5>, zmiany zostaną wyświetlone w GizmosAsync żółtym wyróżnieniu.

protected void Page_Load(object sender, EventArgs e)
{
   var gizmoService = new GizmoService();
   GizmoGridView.DataSource = gizmoService.GetGizmos();
   GizmoGridView.DataBind();
}

Wersja asynchroniczna:

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcAsync));
}

private async Task GetGizmosSvcAsync()
{
    var gizmoService = new GizmoService();
    GizmosGridView.DataSource = await gizmoService.GetGizmosAsync();
    GizmosGridView.DataBind();
}

Następujące zmiany zostały zastosowane, aby strona GizmosAsync mogła być asynchroniczna.

  • Dyrektywa Page musi mieć Async atrybut ustawiony na wartość "true".
  • Metoda RegisterAsyncTask służy do rejestrowania asynchronicznego zadania zawierającego kod, który jest uruchamiany asynchronicznie.
  • Nowa GetGizmosSvcAsync metoda jest oznaczona za pomocą słowa kluczowego asynchronicznego , które nakazuje kompilatorowi generowanie wywołań zwrotnych dla części treści i automatyczne utworzenie zwracanego elementu Task .
  • Element "Async" został dołączony do nazwy metody asynchronicznej. Dołączanie "Async" nie jest wymagane, ale jest konwencją podczas pisania metod asynchronicznych.
  • Zwracany typ nowej GetGizmosSvcAsync metody to Task. Zwracany typ Task reprezentuje bieżącą pracę i zapewnia wywołującym metodę uchwyt, za pomocą którego mogą poczekać na zakończenie operacji asynchronicznej.
  • Słowo kluczowe await zostało zastosowane do wywołania usługi internetowej.
  • Wywołano asynchroniczny interfejs API usługi internetowej (GetGizmosAsync).

GetGizmosSvcAsync Wewnątrz treści metody wywoływana jest inna metoda GetGizmosAsync asynchroniczna. GetGizmosAsync natychmiast zwraca Task<List<Gizmo>>, które ostatecznie zostanie zrealizowane, gdy dane będą dostępne. Ponieważ nie chcesz wykonywać żadnych innych czynności, dopóki nie będziesz mieć danych gizmo, kod czeka na zadanie (przy użyciu słowa kluczowego await ). Słowa kluczowego await można używać tylko w metodach oznaczonych słowem kluczowym async.

Słowo kluczowe await nie blokuje wątku do momentu ukończenia zadania. Rejestruje pozostałą część metody jako wywołanie zwrotne dla zadania i natychmiast zwraca. Po zakończeniu oczekiwanego zadania zostanie uruchomiony mechanizm wywołania zwrotnego, co spowoduje wznowienie wykonywania metody dokładnie w miejscu, w którym zostało przerwane. Aby uzyskać więcej informacji na temat używania słów kluczowych await i async oraz przestrzeni nazw Task, zobacz async referencje.

Poniższy kod przedstawia metody GetGizmos i GetGizmosAsync.

public List<Gizmo> GetGizmos()
{
    var uri = Util.getServiceUri("Gizmos");
    using (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            webClient.DownloadString(uri)
        );
    }
}
public async Task<List<Gizmo>> GetGizmosAsync()
{
    var uri = Util.getServiceUri("Gizmos");
    using (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

Zmiany asynchroniczne są podobne do zmian wprowadzonych w GizmosAsync powyżej.

  • Sygnatura metody została oznaczona słowem kluczowym async, typ zwracany został zmieniony na Task<List<Gizmo>>, a do nazwy metody dołączono Async.
  • Asynchroniczna klasa HttpClient jest używana zamiast synchronicznej klasy WebClient .
  • Słowo kluczowe await zostało zastosowane do metody asynchronicznej HttpClientGetAsync .

Na poniższej ilustracji przedstawiono asynchroniczny widok gizmo.

Zrzut ekranu strony przeglądarki internetowej Gizmos Async, pokazujący tabelę z gizmos oraz odpowiadającymi im szczegółami, wprowadzonymi do kontrolerów interfejsu API.

Prezentacja danych gizmos w przeglądarkach jest identyczna z widokiem utworzonym przez wywołanie synchroniczne. Jedyna różnica polega na tym, że wersja asynchroniczna może być bardziej wydajna w przypadku dużych obciążeń.

RegisterAsyncTask Notatki

Metody podłączone za pomocą RegisterAsyncTask będą uruchamiane natychmiast po preRender.

Jeśli bezpośrednio używasz zdarzeń strony async void, jak pokazano w poniższym kodzie:

protected async void Page_Load(object sender, EventArgs e) {
    await ...;
    // do work
}

nie masz już pełnej kontroli nad tym, kiedy zdarzenia się wykonują. Jeśli na przykład zarówno plik .aspx, jak i plik .Master definiują zdarzenia Page_Load, i jedno lub oba z nich są asynchroniczne, kolejność wykonywania nie może być zagwarantowana. Ma zastosowanie taka sama nieokreślona kolejność obsługi zdarzeń (na przykład async void Button_Click ).

Równoległe wykonywanie wielu operacji

Metody asynchroniczne mają znaczącą przewagę nad metodami synchronicznymi, gdy akcja musi wykonywać kilka niezależnych operacji. W podanym przykładzie strona synchroniczna PWG.aspx (w przypadku produktów, widżetów i Gizmos) wyświetla wyniki trzech wywołań usługi internetowej, aby uzyskać listę produktów, widżetów i gizmos. Projekt ASP.NET Web API, który udostępnia te usługi, używa Task.Delay do symulowania opóźnień lub wolnych połączeń sieciowych. Gdy opóźnienie jest ustawione na 500 milisekund , asynchroniczna strona PWGasync.aspx zajmuje nieco ponad 500 milisekund do ukończenia, podczas gdy wersja synchroniczna PWG przejmuje ponad 1500 milisekund. Strona synchroniczna PWG.aspx jest wyświetlana w poniższym kodzie.

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );
    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();

    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}", 
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

Poniżej przedstawiono kod asynchroniczny PWGasync .

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();
    RegisterAsyncTask(new PageAsyncTask(GetPWGsrvAsync));
    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}",
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

private async Task GetPWGsrvAsync()
{
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var widgetTask = widgetService.GetWidgetsAsync();
    var prodTask = prodService.GetProductsAsync();
    var gizmoTask = gizmoService.GetGizmosAsync();

    await Task.WhenAll(widgetTask, prodTask, gizmoTask);

    var pwgVM = new ProdGizWidgetVM(
       widgetTask.Result,
       prodTask.Result,
       gizmoTask.Result
       );

    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();           
}

Na poniższej ilustracji przedstawiono widok, który zwróciła strona asynchroniczna PWGasync.aspx.

Zrzut ekranu strony przeglądarki internetowej Asynchronous Widgets, Products, and Gizmos, pokazujący tabele Widgets, Products i Gizmos.

Używanie tokenu anulowania

Metody asynchroniczne zwracające Task są anulowalne, ponieważ przyjmują parametr CancellationToken, gdy dostarczany jest z atrybutem AsyncTimeout dyrektywy Page. Poniższy kod przedstawia stronę GizmosCancelAsync.aspx z przekroczeniem czasu wynoszącym jedną sekundę.

<%@ Page  Async="true"  AsyncTimeout="1" 
    Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosCancelAsync.aspx.cs" 
    Inherits="WebAppAsync.GizmosCancelAsync" %>

Poniższy kod przedstawia plik GizmosCancelAsync.aspx.cs .

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcCancelAsync));
}

private async Task GetGizmosSvcCancelAsync(CancellationToken cancellationToken)
{
    var gizmoService = new GizmoService();
    var gizmoList = await gizmoService.GetGizmosAsync(cancellationToken);
    GizmosGridView.DataSource = gizmoList;
    GizmosGridView.DataBind();
}
private void Page_Error(object sender, EventArgs e)
{
    Exception exc = Server.GetLastError();

    if (exc is TimeoutException)
    {
        // Pass the error on to the Timeout Error page
        Server.Transfer("TimeoutErrorPage.aspx", true);
    }
}

W podanej przykładowej aplikacji wybranie linku GizmosCancelAsync wywołuje stronę GizmosCancelAsync.aspx i demonstruje anulowanie (według limitu czasu) wywołania asynchronicznego. Ponieważ czas opóźnienia mieści się w losowym zakresie, może być konieczne odświeżenie strony kilka razy, aby uzyskać komunikat o błędzie przekroczenia limitu czasu.

Konfiguracja serwera dla wywołań usług sieciowych o dużej współbieżności/dużego opóźnienia

Aby zrealizować korzyści wynikające z asynchronicznej aplikacji internetowej, może być konieczne wprowadzenie pewnych zmian w domyślnej konfiguracji serwera. Podczas konfigurowania i testowania obciążeniowego swojej asynchronicznej aplikacji internetowej należy wziąć pod uwagę.

Współautorzy