Асинхронное программирование в .NET - Мотивация и модульное тестирование

Tags: .NET, CPU

Прошло много времени с тех пор, как была выпущена версия .NET 4.5. Чтобы освежить нашу память, напомним, что это произошло 15 августа 2012 года. Да, шесть лет назад. Чувствуете себя старым? Нет, это не намерение отправить вас на пенсию, а напомнить вам о некоторых моментах выпуска .NET. Одной из основных особенностей, которые принесла  эта версия, было асинхронное программирование с использованием методов async/wait. В основном, ребята из Microsoft заставили работать компилятор, что раньше делали разработчики, поддерживая логическую структуру, которая напоминает синхронный код.

Понимаете, в то время Windows Phone все еще оставалась хорошей штукой, и приложения для этих платформ имели определенные ограничения. Основным из них заключалось в том, что Windows Phone, в отличие от настольных приложений, вводила жесткий предел, в котором ни один метод не мог блокировать более 50 мс. Это, в свою очередь, означало, что больше не было блокировки пользовательского интерфейса для разработчиков, что привело к необходимости какой-то асинхронности кода. .NET 4.5 представляет собой ответ на эту необходимость.

Итак, почему мы пишем о том, что произошло более чем половину десятилетия назад? Ну, мы заметили, что, хотя это старая тема, еще много инженеров продолжают бороться с концепцией. Цитируя Майка Джеймса от iProgrammer:

Часто программист полностью осознает, что то, что он делает, ориентировано на объекты, но только смутно осознает, что он пишет асинхронный код.

Вот почему мы попытаемся подытожить самые важные части этого стиля программирования в нескольких блогах. Кроме того, мы попытаемся определить ситуации, в которых мы должны использовать этот стиль программирования, а также ситуации, когда мы должны избегать этого. Итак, давайте погрузимся в это!

Мотивация и Прецеденты

По сути, этот стиль программирования применим в любом месте, где вам нужно что-то сделать, ожидая чего-то еще, чтобы завершить. Годы использования этого инструмента фактически дали нам возможность понять, где мы должны его использовать. К примеру, Это отлично подходит для пользователей, и в целом для систем на основе событий, например параллелизм на основе процессора или работа с файлами.

Любая деятельность, которая потенциально может блокировать, например доступ к сети, является хорошим кандидатом для такого подхода. Если какой-либо из этих блокирующих действий заканчивается синхронным процессом, все приложение блокируется. С другой стороны, если это действие является частью асинхронного процесса, приложение может продолжать работать с другими задачами до тех пор, пока не будет выполнено действие.

Конечно, вам не нужно злоупотреблять этой возможностью. Например, мы не должны асинхронно обновлять записи, которые являются зависимыми. Это, как правило, плохая идея, и наши данные очень быстро выйдут из синхронизации. Кроме того, этой идеей иногда злоупотребляют. Например, если мы работаем над некоторыми простыми действиями и операциями, мы должны рассмотреть более ортодоксальные подходы. Использование асинхронных концепций в этих случаях может, по сути, налагать дополнительные накладные расходы нежели выгоды.

Типы Async/Await и Return

Асинхронный механизм реализован в .NET с использованием ключевых слов async/await в нашем коде. Мы используем оператор async для обозначения наших методов как асинхронных. Только методы с помощью этих методов оператора могут использовать в них оператор ожидания. С другой стороны, оператор Await сообщает компилятору, что метод асинхронного использования, в котором он был использован, не может продолжаться до этой точки, пока ожидаемая асинхронная задача не будет завершена. В принципе, он приостанавливает выполнение метода до тех пор, пока не будет выполнена ожидаемая задача. Методы, отмеченные асинхронным оператором, также можно вызвать с помощью оператора ожидания из других методов.

Еще одна важная вещь, которую следует упомянуть, заключается в том, что методы async должны возвращать класс Task или Task <TResult>. Это связано с тем, что в этом методе ожидаемый оператор применяется к Task, которая возвращается из другого метода async. Чтобы подвести итог, асинхронный механизм реализуется в .NET следующим образом:

  • Мы используем оператор async для обозначения методов, которые мы хотим сделать асинхронными. Эти методы должны возвращать класс Task или Task <TResult>.
  • Когда мы вызываем методы, отмеченные оператором async, мы используем оператор ожидания. Этот оператор выполнит задачу, возвращаемую асинхронным методом.

Как это выглядит в коде? Взгляните на этот пример класса WebAccess.cs:

public class WebAccess
{
    public async Task<int> AccessRubiksCodeAsync()
    {
        HttpClient client = new HttpClient();

        var getContent = client.GetStringAsync("http://rubikscode.net");

        LogToConsole("Yay!");

        string content = await getContent;

        return content.Length;
    }

    private void LogToConsole(string message)
    {
        Console.WriteLine("At the moment I am actually listening to the new NIN song...");
        Console.WriteLine("It is pretty cool...like something of David Bowie's - Blackstar.");
        Console.WriteLine("message");
    }
}

Цель этого класса - получить доступ к этому веб-сайту неблокирующим образом и получить длину тела ответа, для которого используется метод AccessRubiksCodeAsync. Мы добавили суффикс Async в конце имени функции, которая является стандартным соглашением именования для асинхронных методов. Метод AccessRubiksCodeAsync вызывает некоторые операции, которые также синхронны, как функция LogToConsole. Важно понимать, что метод GetStringAsync также асинхронен и возвращает задачу. Эта задача позже запускается оператором ожидания.


Обратите внимание на то, как мы достигли структуры синхронного кода, что является удивительным. Несмотря на то, что мы вызываем оператора ожидания и, по сути, выполняем асинхронную задачу, наш код выглядит довольно аккуратно и просто. Это одна из причин, по которой механизм async/await получил большую популярность.Цель этого класса - получить доступ к этому веб-сайту неблокирующим образом и получить длину тела ответа, для которого используется метод
AccessRubiksCodeAsync. Мы добавили суффикс Async в конце имени функции, которая является стандартным соглашением именования для асинхронных методов. Метод AccessRubiksCodeAsync вызывает некоторые операции, которые также синхронны, как функция LogToConsole. Важно понимать, что метод GetStringAsync также асинхронен и возвращает задачу. Эта задача позже запускается оператором ожидания.

 

Итак, что произойдет, если метод GetStringAsync слишком долго отвечает? Вот как происходит рабочий процесс:

На первом этапе (с пометкой 1) метод AccessRubiksCodeAsync создает экземпляр HttpClient и вызывает метод GetStringAsync этого класса. Цель состоит в том, чтобы загрузить содержимое веб-сайта в виде строки. Если произойдет что-то неожиданное, это блокирует метод GetStringAsync, например если веб-сайт слишком долго загружается, этот метод позволяет управлять своим абонентом. Таким образом, это позволяет избежать блокировки ресурсов. Кроме того, этот метод возвращает Task, который AccessRubiksCodeAsync присваивает переменной getContent. Позже в коде эта переменная используется в сочетании с ожидающим оператором.

Теперь, поскольку мы еще не использовали оператор ожидания на getContent, AccessRubiksCodeAsync может продолжить другие операции, которые не зависят от результата задачи getContent. Таким образом, метод LogToConsole может запускаться синхронно (шаг 2). Это означает, что этот метод возьмет контроль, выполнит свою работу и только затем вернет управление методу AccessRubiksCodeAsync.

После этого этот метод вызывает getContent с ожидающим оператором (шаг 3). Это означает, что в этот момент этот метод требует результата метода GetStringAsync. Если этот GetStringAsync все еще не готов, AccessRubiksCodeAsync приостанавливает свой прогресс и возвращает управление своему вызывающему. Разумеется, преимущество использования такого рода потока заключается в том, что мы «дали некоторое время» методу GetStringAsync, а между тем мы выполнили синхронные части кода. Когда контент загружается, в результате его длина возвращается (шаг 4).

Модульное тестирование асинхронных методов

Модульное тестирование на самом деле является отличным примером того, как инициируются асинхронные методы. В этом примере был использован xUnit, но механизм async/await поддерживается в других модульных системах тестирования, таких как NUnit и MSTests. Если вы хотите установить xUnit в свой проект, введите эти команды в консоли диспетчера пакетов:

Install-Package xunit
Install-Package xunit.runner.console
Install-Package xunit.runner.visualstudio
Update-Package

Хорошо, это должно привести вас к скорости с помощью xUnit. Теперь давайте посмотрим на наш тестовый класс для класса WebAccess - WebAccessTests.

public class WebAccessTests
{
    [Fact]
    public async Task AccessRubiksCode_NoCondition_ExpectedResult()
    {
        var webAccess = new WebAccess();

        var result = await webAccess.AccessRubiksCodeAsync();

        Assert.NotEqual(0, result);
    }
}

 

Теперь давайте проанализируем важные части этого теста. Во-первых, обратите внимание на то, как наш тестовый метод отмечен как  public async Task, в отличие от стандартного public void. Это делается так, потому что в нашем методе тестирования мы фактически вызываем метод AccessRubiksCodeAsync класса WebAccess с оператором await. Мы могли бы пропустить это и вызвать этот метод без оператора await и сохранить понятие  public void , но это не будет в конечном итоге в поведении, которое мы ожидали.

Что произойдет, если мы это сделаем - вызовем метод async без оператора  await? Ну, в этом случае этот метод async будет выполняться как синхронный метод, то есть он будет блокировать поток. Опять же, это плохо для пользовательского интерфейса и веб-операций, и масштабирование становится невозможным. Это, конечно, можно использовать как преимущество в определенных ситуациях, но важно знать, как работает этот механизм.

Кроме того, в тестовую структуру нет существенных изменений. Сначала мы создаем экземпляр класса WebAccess в фазе  “arrange” . В фазе  “act” мы используем оператор await для запуска метода AccessRubiksCodeAsync и получения результата. Наконец, в фазе “assert” мы проверяем справедливость результата.

Вывод

Асинхронное программирование - это в некоторой мере стандартная функциональность .NET за несколько лет. Тем не менее, иногда я понимаю, что менее опытные программисты не совсем понимают это. Особенно те, которые не имеют опыта работы с некоторыми другими технологиями, где этот механизм непосредственно используется в самой технологии, например Node.js. Или они знакомы с этим и пытаются использовать его в любой ситуации.

В этой статье мы разобрали, как асинхронное программирование выполняется в .NET-мире с высокого уровня. В следующих нескольких статьях мы углубимся в тему и рассмотрим некоторые другие аспекты этого стиля.

No Comments

Add a Comment