Прочитав статью об изучении MVC и увидев комментарий пользователя RouR, я очень заинтересовался данной темой, ну и на ходу решил перевести указанную им оригинальную статью.
Если и тебе, дорогой читатель, это интересно — прошу под кат!
«Я попробую показать вам, что нового несет в себе C# 5.0 с точки зрения асинхронного программирования при использовании ключевого слова await. Особенно для веб-приложений ASP.NET MVC 4.»
Я некоторое время поигрался с Visual Studio Async CTP и теперь у меня достаточно соображений о том, как он работает, и как его грамотно использовать. И это значит, что я могу написать об этом.
Главная проблема большинства нашего ПО — операции, требующие длительного времени исполнения. Пользователь запускает такую операцию — и она блокирует основной поток программы до тех пор, пока не закончится ее выполнение. И в результате мы получаем неудобное пользователю ПО и недовольных клиентов. Я уверен, что каждый из нас сталкивался с такими программами!
Асинхронное программирование — далеко не новая парадигма, она уже была во времена .NET 1.0. Я не очень интересовался программированием 3 года назад, так что это понятие для меня достаточно ново и до сих пор. Насколько я читал, концепция развивалась во многом с точки зрения .NET на асинхронное программирование и, как мне кажется, находится сейчас на лучшей своей стадии.
С одной стороны, в асинхронном программировании и так очень просто запутаться, как это уже случалось со мной несколько раз. Я ходил туда-сюда и много думал о том, как же эта штука работает и где ее применить. И мне хочется верить, что в конце концов я все верно понял. С другой стороны, еще проще запутаться в асинхронном программировании, если применять его в веб-приложениях, ведь у нас нет потока UI, в который мы немедленно могли бы вывести результаты работы. Асинхронно или нет, но пользователь должен ждать некоторое время. Именно поэтому асинхронное программирование нежелательно для веб-приложений. И если вы тоже так думаете (а я прежде и сам так считал), значит вы еще ничего не поняли!
Если ваша операция выполняется синхронно и занимает много времени, то у вас нет другого выбора, как блокировать основной поток до ее завершения. А если вы делаете ту же операцию асинхронно, то это означает, что вы запускаете операцию и продолжаете что-то делать, возвращаясь к результатам вашей операции только тогда, когда она уже завершится. Между запуском и окончанием операции ваш поток свободен и может делать другие вещи. Я думаю, что большая часть людей путается именно здесь. Создание дополнительных потоков — дело накладное и может привести к проблемам, но мне кажется, что это уже в прошлом. В .NET 4.0 резко увеличили лимит на количество запускаемых потоков, но это вовсе не значит, что везде и всюду нужно создавать множество потоков — по крайней мере теперь не стоит переживать при создании очередного потока. Сейчас идут (или мне лучше сказать — уже прошли?) много споров об асинхронном программировании:
- Должны ли быть асинхронными обращения к моей БД?
- Должны ли быть асинхронными обращения к моей БД (часть 2)?
- Как асинхронные операции в ASP.NET MVC используют потоки из пула .NET 4 (ThreadPool)?
- В чем недостатки использования ExecuteReaderAsync в C# AsyncCTP?
Я не знаю точного ответа на все эти вопросы, но вот вам пара примеров:
- Windows Runtime (WinRT) разрабатывался с хорошей поддержкой асинхронности. Все, что выполняется дольше 40мс (операции, связанные с сетью, файловой системой — то есть весь основной ввод-вывод) — все это должно быть асинхронным!
- ASP.NET Web API позволяет отдавать данные вашего сервиса в нескольких форматах. Единственное, что следует отметить — в нем практически нет синхронных вызовов. Буквально (не в переносном смысле) — просто нет синхронных вызовов для некоторых методов!
Вот, как говорится, пора бы нам в том или ином виде воспользоваться преимуществами асинхронности. Так в чем же проблема? А проблема в том, как работает данная асинхронная модель. Как говорит Андерс Хейлсберг, эта программная модель выворачивает наш код наизнанку. Не говоря уже о том, как очень трудно сделать вложенные вызовы асинхронных операций с обработкой исключений.
Visual Studio Async CTP и как он работает
В C# 5.0 у нас будет новая модель асинхронного программирования, очень похожая на синхронную. А пока разработчики выпустили CTP для текущей версии, который доступен по Go-Live лицензии. Вот цитата из спецификации AsyncCTP:
"Асинхронные функции — новая возможность в C#, которая позволяет легко описывать асинхронные вызовы. Когда выполнение функции дойдет до выражения с ключевым словом await и начнется его выполнение, остальная часть функции будет перестроена, как автоматическое продолжение выполняемой операции и произойдет моментальный возврат из асинхронной функции, в то время как сама функция будет продолжать выполняться. Другими словами — теперь работу по выстраиванию порядка выполнения кода на себя берет сам язык, а не программист. В результате чего асинхронный код приобретает логическую структуру."
Асинхронная функция — это метод класса или анонимная функция, помеченные модификатором async. Она может возвращать как объект класса Task, так и Task<T> для произвольного типа T — в таких функциях может быть await. Если же функция, помеченная async, возвращает void (что также допустимо) — тогда использовать await не удастся. Но с другой стороны, мы же можем в качестве T использовать любой тип.
ASP.NET MVC 4 и асинхронные возможности C#
Грамотное использование асинхронности в приложениях ASP.NET MVC может очень позитивно сказаться на производительности. Верите вы или нет, но это так. Сейчас я покажу как.
Так почему же мы не везде и все делаем асинхронно? А потому что это очень тяжело, чревато ошибками и в конечном счете трудно потом поддерживать наше приложение. Но с новой асинхронной моделью это скоро изменится.
В ASP.NET MVC 4 асинхронное программирование сильно изменилось. Возможно вы знаете, что в ASP.NET MVC 3 наш контроллер должен быть унаследован от AsyncController и должен соответствовать определенному шаблону. Об этом можно прочитать в статье "Использование асинхронных контроллеров в ASP.NET MVC".
В ASP.NET MVC 4 нам не нужно больше использовать AsyncController, достаточно наш контроллер пометить ключевым словом async и вернуть объект класса Task или Task<T> (где T — обычно ActionResult).
Я попробовал в одном приложении совместить сразу два примера, которые делают одно и то же действие — синхронно и асинхронно, соответственно. А также я провел нагрузочное тестирование, и результаты шокировали меня! Теперь давайте взглянем на код.
Во-первых, я создал простейший REST-сервис, использую новый ASP.NET Web API. Простейшая модель, которая хранит коллекцию в памяти (можно было использовать БД вместо этого):
public class Car
{
public string Make;
public string Model;
public int Year;
public int Doors;
public string Colour;
public float Price;
public int Mileage;
}
public class CarService
{
public List<Car> GetCars()
{
List<Car> Cars = new List<Car>
{
new Car
{
Make="Audi",
Model="A4",
Year=1995,
Doors=4,
Colour="Red",
Price=2995f,
Mileage=122458
},
new Car
{
Make="Ford",
Model="Focus",
Year=2002,
Doors=5,
Colour="Black",
Price=3250f,
Mileage=68500
},
new Car
{
Make="BMW",
Model="5 Series",
Year=2006,
Doors=4,
Colour="Grey",
Price=24950f,
Mileage=19500
}
//This keeps going like that
};
return Cars;
}
}
И код сервиса:
public class CarsController : ApiController
{
public IEnumerable<Car> Get()
{
var service = new CarService();
return service.GetCars();
}
}
Итак, у меня есть сервис. Теперь я напишу веб-приложение, которое будет получать данные от сервиса и показывать их. Для этого я написал класс сервиса, который получает данные и десериализует их: для асинхронных вызовов я использовал новый HttpClient, а для синхронных — привычный WebClient. Также я использовал Json.NET:
public class CarRESTService
{
readonly string uri = "http://localhost:2236/api/cars";
public List<Car> GetCars()
{
using (WebClient webClient = new WebClient())
{
return JsonConvert.DeserializeObject<List<Car>>
(
webClient.DownloadString(uri)
);
}
}
public async Task<List<Car>> GetCarsAsync()
{
using (HttpClient httpClient = new HttpClient())
{
return JsonConvert.DeserializeObject<List<Car>>
(
await httpClient.GetStringAsync(uri)
);
}
}
}
Про метод GetCars сказать нечего — он прост. А вот про GetCarsAsync можно сказать следующее:
- Он помечен ключевым словом async, которое говорит нам, что тут есть некий асинхронный код.
- Мы использовали ключевое слово await перед HttpClient.GetStringAsync, который возвращает Task<string>. И мы можем использовать его, как обычный string — ключевое слово await нам это позволяет.
Ну и, наконец, вот он контроллер:
public class HomeController : Controller
{
private CarRESTService service = new CarRESTService();
public async Task<ActionResult> Index()
{
return View("index", await service.GetCarsAsync());
}
public ActionResult IndexSync()
{
return View("index", service.GetCars());
}
}
Итак, у нас получился Index — асинхронная функция, возвращающая Task<ActionResult>, и IndexSync — обычный синхронный вызов. Если мы перейдем по адресу /home/index или /home/indexsync, то мы не увидим особой разницы в скорости открытия страниц — она примерно одинакова.
Чтобы измерить разницу в работе этих методов, я создал нагрузочные тесты в MS Visual Studio 2010 Ultimate. Я буду в течение двух минут открывать эти две страницы, начиная с нагрузки в 50 пользователей, постепенно увеличивая ее по 20 пользователей раз в 5 секунд. Максимальным пределом будет величина в 500 пользователей. Смотрим результат (кликабельно):
Видно, что для синхронного варианта время отклика составляет 11.2 секунды, а для асинхронного — 3.65 секунды. Я думаю, разница очевидна.
Вот с этого момента я стал приверженцем концепции «Все, что выполняется дольше 40мс (операции, связанные с сетью, файловой системой — то есть весь основной ввод-вывод) — все это должно быть асинхронным!»
Автор: diger_74