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:

