Асинхронное программирование в приложениях ASP.NET MVC 4

в 6:22, , рубрики: .net, ASP, asp.net mvc 4, async ctp

Прочитав статью об изучении MVC и увидев комментарий пользователя RouR, я очень заинтересовался данной темой, ну и на ходу решил перевести указанную им оригинальную статью.

Если и тебе, дорогой читатель, это интересно — прошу под кат!

«Я попробую показать вам, что нового несет в себе C# 5.0 с точки зрения асинхронного программирования при использовании ключевого слова await. Особенно для веб-приложений ASP.NET MVC 4.»


Я некоторое время поигрался с Visual Studio Async CTP и теперь у меня достаточно соображений о том, как он работает, и как его грамотно использовать. И это значит, что я могу написать об этом.

Главная проблема большинства нашего ПО — операции, требующие длительного времени исполнения. Пользователь запускает такую операцию — и она блокирует основной поток программы до тех пор, пока не закончится ее выполнение. И в результате мы получаем неудобное пользователю ПО и недовольных клиентов. Я уверен, что каждый из нас сталкивался с такими программами!

Асинхронное программирование — далеко не новая парадигма, она уже была во времена .NET 1.0. Я не очень интересовался программированием 3 года назад, так что это понятие для меня достаточно ново и до сих пор. Насколько я читал, концепция развивалась во многом с точки зрения .NET на асинхронное программирование и, как мне кажется, находится сейчас на лучшей своей стадии.

С одной стороны, в асинхронном программировании и так очень просто запутаться, как это уже случалось со мной несколько раз. Я ходил туда-сюда и много думал о том, как же эта штука работает и где ее применить. И мне хочется верить, что в конце концов я все верно понял. С другой стороны, еще проще запутаться в асинхронном программировании, если применять его в веб-приложениях, ведь у нас нет потока UI, в который мы немедленно могли бы вывести результаты работы. Асинхронно или нет, но пользователь должен ждать некоторое время. Именно поэтому асинхронное программирование нежелательно для веб-приложений. И если вы тоже так думаете (а я прежде и сам так считал), значит вы еще ничего не поняли!

Если ваша операция выполняется синхронно и занимает много времени, то у вас нет другого выбора, как блокировать основной поток до ее завершения. А если вы делаете ту же операцию асинхронно, то это означает, что вы запускаете операцию и продолжаете что-то делать, возвращаясь к результатам вашей операции только тогда, когда она уже завершится. Между запуском и окончанием операции ваш поток свободен и может делать другие вещи. Я думаю, что большая часть людей путается именно здесь. Создание дополнительных потоков — дело накладное и может привести к проблемам, но мне кажется, что это уже в прошлом. В .NET 4.0 резко увеличили лимит на количество запускаемых потоков, но это вовсе не значит, что везде и всюду нужно создавать множество потоков — по крайней мере теперь не стоит переживать при создании очередного потока. Сейчас идут (или мне лучше сказать — уже прошли?) много споров об асинхронном программировании:

Я не знаю точного ответа на все эти вопросы, но вот вам пара примеров:

  • 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 пользователей. Смотрим результат (кликабельно):

image

Видно, что для синхронного варианта время отклика составляет 11.2 секунды, а для асинхронного — 3.65 секунды. Я думаю, разница очевидна.

Вот с этого момента я стал приверженцем концепции «Все, что выполняется дольше 40мс (операции, связанные с сетью, файловой системой — то есть весь основной ввод-вывод) — все это должно быть асинхронным!»

Автор: diger_74

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js