ASP MVC Asynchroniczne Akcje
W Visual Studio 2012 zostały wprowadzone dwa nowe słowa kluczowe: async i await. Modyfikator async stwierdza, że metoda jest asynchroniczna, natomiast await służy do wywołania takiej metody. Drobnej modyfikacji wymaga też typ zwrotny, a dokładniej wymaga dodania typu Task.
Przed przedstawieniem pierwszego przykładu odpowiem jeszcze, po co stosować metody asynchroniczne? Odpowiedz jest prosta – ułatwiają one programowanie współbieżne/wielowątkowe.
Opisywany przykład opiera się na aplikacji ASP MVC. Porównałem w niej szybkość działania akcji synchronicznych i asynchronicznych. Aplikacja służy do pobierania temperatury dla zadanego miasta od trzech dostawców i obliczeniu średniej. Pierwszym krokiem jest stworzenie nowego projektu typu ASP MVC. Struktura gotowej aplikacji będzie wyglądała tak jak na poniższym rysunku:
Następnie należy utworzyć kontroler o nazwie WeatherController:
[UseStopwatch] [OutputCache(Location = OutputCacheLocation.None, NoStore = true)] public class WeatherController : Controller { }
W przykładzie będziemy mierzyć czas wykonywania akcji. Dlatego należy zapobiec cachowaniu stron. Do tego celu służy ustawienie OutputCache. Dodano także atrybut UseStopwatchAttribute:
public class UseStopwatchAttribute : System.Web.Mvc.ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); filterContext.Controller.ViewBag.stopWatch = stopWatch; } public override void OnResultExecuting(ResultExecutingContext filterContext) { Stopwatch stopWatch = (Stopwatch)filterContext.Controller.ViewBag.stopWatch; stopWatch.Stop(); double et = stopWatch.Elapsed.Seconds + (stopWatch.Elapsed.Milliseconds / 1000.0); filterContext.Controller.ViewBag.elapsedTime = et.ToString(); } }
Atrybut służy do mierzenia czasu wykonywania akcji. Wykorzystuje on klasę diagnostyczną Stopwatch.
Pierwsza akcja będzie pobierała temperatury w sposób synchroniczny:
public ActionResult GetForecast(string city = "Tarnów") { ViewBag.MethodType = "Synchronous"; var service1 = new SourceService1(); var service2 = new SourceService2(); var service3 = new SourceService3(); var viewModel = new ForecastViewModel( service1.GetTemperatureByCity(city), service2.GetTemperatureByCity(city), service3.GetTemperatureByCity(city) ); return View("Forecast", viewModel); }
Na potrzeby aplikacji stworzono trzy klasy które symulują pobieranie temperatur od zewnętrznych dostawców (SourceService1-3):
public class SourceService1 { private readonly int Sleep = 3000; private readonly double ReturnTemperature = 24.1; public double GetTemperatureByCity(string city) { System.Threading.Thread.Sleep(Sleep); return ReturnTemperature; } public async Task GetTemperatureByCityAsync(string city) { await Task.Delay(Sleep); return ReturnTemperature; } } public class SourceService2 { private readonly int Sleep = 2800; private readonly double ReturnTemperature = 23.5; public double GetTemperatureByCity(string city) { System.Threading.Thread.Sleep(Sleep); return ReturnTemperature; } public async Task GetTemperatureByCityAsync(string city) { await Task.Delay(Sleep); return ReturnTemperature; } } public class SourceService3 { private readonly int Sleep = 3300; private readonly double ReturnTemperature = 24.1; public double GetTemperatureByCity(string city) { System.Threading.Thread.Sleep(Sleep); return ReturnTemperature; } public async Task GetTemperatureByCityAsync(string city) { await Task.Delay(Sleep); return ReturnTemperature; } }
Zawierają one metody: GetTemperatureByCity, GetTemperatureByCityAsync. Wykonują one to samo zadanie, z tą różnicą że jedna jest synchroniczna a druga asynchroniczna. Każda z metod jest zatrzymywana na okres około trzech sekund, ma to na celu symulowanie opóźnień od zewnętrznych dostawców temperatur.
Kolejną klasą jest ForecastViewModel:
public class ForecastViewModel { [DisplayName("Temperature 1")] public double Temperature1 { get; set; } [DisplayName("Temperature 2")] public double Temperature2 { get; set; } [DisplayName("Temperature 3")] public double Temperature3 { get; set; } [DisplayName("Avg Temperature")] public double AvgTemperature { get { double[] array = { Temperature1, Temperature2, Temperature3 }; return array.Average(); } } public ForecastViewModel(double t1, double t2, double t3) { Temperature1 = t1; Temperature2 = t2; Temperature3 = t3; } }
Jest to prosta klasa która przetrzymuje informacje o temperaturach. Ostatnim elementem jest widok Forecast.cshtml:
@model WeatherAsync.Models.ForecastViewModel @{ ViewBag.Title = "Forecast"; }
Forecast
<fieldset><legend>Temperature</legend> <h4>Method Type: @ViewBag.MethodType</h4> <h4>Execution Time: @ViewBag.elapsedTime</h4> <div class="display-label">@Html.DisplayNameFor(model => model.Temperature1)</div> <div class="display-field">@Html.DisplayFor(model => model.Temperature1)</div> <div class="display-label">@Html.DisplayNameFor(model => model.Temperature2)</div> <div class="display-field">@Html.DisplayFor(model => model.Temperature2)</div> <div class="display-label">@Html.DisplayNameFor(model => model.Temperature3)</div> <div class="display-field">@Html.DisplayFor(model => model.Temperature3)</div> <div class="display-label">@Html.DisplayNameFor(model => model.AvgTemperature)</div> <div class="display-field">@Html.DisplayFor(model => model.AvgTemperature)</div> </fieldset>
Wyświetla on informację: o typie akcji, czasie wykonania i temperaturach:
Jak widać akcja wykonała się w czasie 9,415 sekund. Ostatnim elementem będzie implementacja akcji asynchronicznej:
public async Task GetForecastAsync(string city = "Tarnów") { ViewBag.MethodType = "Asynchronous"; var service1 = new SourceService1(); var service2 = new SourceService2(); var service3 = new SourceService3(); var serv1 = service1.GetTemperatureByCityAsync(city); var serv2 = service2.GetTemperatureByCityAsync(city); var serv3 = service3.GetTemperatureByCityAsync(city); await Task.WhenAll(serv1, serv2, serv3); var viewModel = new ForecastViewModel( serv1.Result, serv2.Result, serv3.Result ); return View("Forecast", viewModel); }
Najciekawszym elementem w tej akcji jest await Task.WhenAll(serv1, serv2, serv3). Wszystkie trzy elementy są przetwarzane równolegle, przez co teoretycznie można w tym przypadku uzyskać trzy krotną redukcje potrzebnego czasu. A jak jest w praktyce? Bardzo dobrze: