Как я понимаю асинхронный код? -4



Привет, Хабр! Представляю вашему вниманию перевод (с небольшими корректировками) статьи «How Do I Think About Async Code?!» автора Leslie Richardson.

Асинхронный код становится все более популярным для написания отзывчивых приложений. К сожалению, асинхронное программирование так же привносит дополнительные трудности. Как следствие, понять, как работает такой код, может быть непростой задачей, вне зависимости от вашего опыта. Если вы только начали работать с асинхронным кодом, или вы захотели освежить свое понимание – это введение в мир асинхронного программирования!

Что такое асинхронный код?


Асинхронное программирование позволяет вам выполнить блок кода без остановки (или блокировки) всего потока, в котором выполняется действие. Распространенный миф об асинхронном коде заключается в том, что он улучшает производительность, что не всегда верно. Вместо этого главная особенность асинхронного программирования заключается в том, что оно увеличивает количество задач (пропускную способность), которые могут выполняться одновременно, без необходимости блокировать поток, в котором эти действия выполняются.

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

Почему мне стоит использовать асинхронный код? Пример, пожалуйста!


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

Синхронный метод MakeCake()

image

Синхронная программа выпекания пирога

image

В реальной жизни вы, как правило, разделяете этот процесс на задачи, замешиваете тесто, пока духовка разогревается. Или делаете глазурь, в то время как пирог запекается в духовке. Это увеличивает вашу производительность и позволяет испечь торт намного быстрее. Это как раз тот случай, где асинхронный код пригодится! Сделав наш текущий код асинхронным, мы сможем заняться другими делами, чтобы скоротать время, в то время пока мы ожидаем(await) результата задачи(task), такой как выпекание пирога в духовке.
Чтобы сделать это – изменим наш код, а так же добавим метод PassTheTime. Теперь наш код сохраняет состояние задачи, запускает другую синхронную или асинхронную операцию и получает результат сохраненной задачи, в тот момент, когда это необходимо.

Асинхронный метод MakeCake()
image

Асинхронная программа выпечки пирога
image

По сравнению с синхронным методом MakeCake, в котором отсутствует метод PassTheTime, асинхронному методу MakeCakeAsync удается выполнить больше задач, не блокируя поток, что сокращает время, необходимое для выполнения всего метода в целом.

Сравнение асинхронной и синхронной программ

image

Как мне писать асинхронный код в .NET?


C # позволяет писать асинхронный код, используя тип Task и ключевые слова await и async. Тип Task сообщает вызывающей стороне о возможном типе возвращаемого значения. Он также указывает на то, что другие действия могут продолжать выполняться в вызвавшем его методе. Ключевое слово async работает в паре с ключевым словом await, которое уведомляет компилятор о том, что нам потребуется возвращаемое методом значение, но не сразу. В результате нам не нужно блокировать вызывающий поток, и мы можем продолжать выполнение других задач, пока не потребуется ожидаемое значение. Первоначально асинхронный метод будет выполняться синхронно, пока не будет найдено ключевое слово await. Это именно тот момент, когда выполнение метода начнется асинхронно.

Я узнал об асинхронном коде! Что теперь?


Хотя асинхронное приложение выпечки пирога это прекрасно, но есть много других реальных приложений, которые также используют асинхронный код. Два наиболее распространенных примера:

Приложения, использующие HTTP-запросы — в зависимости от запроса, обработка HTTP-вызовов может занять длительное время. Использование асинхронного кода позволяет нам выполнить другие операции, пока мы ожидаем ответа от сервера.

Пример запроса HTTP GET

image

Приложения, с пользовательским интерфейсом — приложения WPF или любые другие, использующие кнопки, текстовые поля и другие ресурсы UX, также отлично подходят для асинхронной реализации. Например, приложение WPF, производящее анализ файла. Данная процедура может занять некоторое время. Однако, сделав это действие асинхронным, вы по-прежнему сможете взаимодействовать с пользовательским интерфейсом, не останавливая приложение полностью, во время ожидания завершения операции.

Теги:




Комментарии (8):

  1. solderman
    /#21914410

    Ага блинк без дэлэя и наступает счастье. Надо только следить, чтобы вызываемые участки кода были неблокирующие и в случае непредвиденного зависания разруливать ситуацию.
    Например добавить вачдог на тиньке, который в проблемной ситуации будет перезапускать всю систему.

  2. shapovalex
    /#21914424

    Есть одна проблема. Await блокирует вызывающий поток и весь смысл асинхронности теряется. Так что код который вы написали не асинхронный аж никак.

    • mayorovp
      /#21914516

      Await блокирует вызывающий поток

      С какого перепугу?

    • lair
      /#21914562

      Await блокирует вызывающий поток и весь смысл асинхронности теряется

      await в C# не блокирует вызывающий поток.

  3. SLenik
    /#21918902

    Наверное, мои вопросы будут скорее к оригиналу, нежели к Вашему переводу. Но всё же.


    1. Отсутствует полный исходный код примеров. В частности, что в оригинале, что в переводе отсутствует код метода PassTheTime.


    2. Асинхронность в .net, к большому сожалению не настолько проста, как описывается в статье =(( Наверное, стоило бы в заключение дать ссылки либо на другие примеры использования, либо на более глубоко разбираемые примеры по async-await.



    В приведенном примере синхронного кода если, например, внутри метода BakeCake() вывалится Exception — это прервет выполнение всех последующих методов.


    А вот для асинхронного кода уже начинаются вариации. Похожий пример описан в блоге Stephen Cleary: https://blog.stephencleary.com/2016/12/eliding-async-await.html (раздел Exceptions).


    Поясню на примере.


    Пусть код метода BakeCakeAsync выглядит так:


    public static async Task<bool> BakeCakeAsync(bool isPreheated)
    {
      if (!isPreheated)
        throw new InvalidOperationException("before 1st await");
      var result = await InnerBakeCakeAsync(isPreheated).ConfigureAwait(false);
      if (isPreheated)
        throw new InvalidOperationException("after await");
      return result;
    }

    Так вот: если метод вызовется isPreheated = true, то исключение в основном методе "поймается" на строке


    Task<bool> bakeCakeTask = BakeCakeAsync(isPreheated);

    И AddFrostingIngredients не будет вызван.


    А вот если вызвать BakeCakeAsync вызовется с isPreheated = false, то исключение "поймается" уже только на строке с await-ом:


    bool isBaked = await bakeCakeTask;

    И получается, что AddFrostingIngredients вполне себе успеет выполниться


    То есть при определенных условиях мы подготовим замороженные ингредиенты к приготовлению не посмотрев даже, что с процессом выпекания что-то не так.

    • mayorovp
      /#21919892

      Так вот: если метод вызовется isPreheated = true, то исключение в основном методе "поймается" на строке Task<bool> bakeCakeTask = BakeCakeAsync(isPreheated); И AddFrostingIngredients не будет вызван.

      Вы это проверяли? Насколько я знаю, в любом случае исключение будет "спрятано" в возвращаемый Task, независимо от положения относительно первого await.

      • lair
        /#21919920

        Вы это проверяли?

        Там все немного хитрее. Если BakeCakeAsyncasync, то обернется. А если нет, то есть вот так:


        public static Task<bool> BakeCakeAsync(bool isPreheated)
        {
          if (!isPreheated)
            throw new InvalidOperationException();
          return InnerBakeCakeAsync(isPreheated);
        }

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


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

      • SLenik
        /#21920374

        mayorovp, прошу прощения, я привёл неверный пример. lair в комментарии ниже привел правильный.