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:
struktura-WeatherAsync
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:
sync-temperature
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:
Async-GetForecast

Share Button

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Time limit is exhausted. Please reload the CAPTCHA.