Tw-city.info

IT Новости
0 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Асинхронное программирование c

Асинхронность в программировании

    Статьи, 20 декабря 2018 в 18:04

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

При вызове методов read() и write() текущий поток исполнения будет прерван в ожидании ввода-вывода по сети. Причём большую часть времени программа будет просто ждать. В высоконагруженных системах чаще всего так и происходит — почти всё время программа чего-то ждёт: диска, СУБД, сети, UI, в общем, какого-то внешнего, независимого от самой программы события. В малонагруженных системах это можно решить созданием нового потока для каждого блокирующего действия. Пока один поток спит, другой работает.

Но что делать, когда пользователей очень много? Если создавать на каждого хотя бы один поток, то производительность такого сервера резко упадёт из-за того, что контекст исполнения потока постоянно сменяется. Также на каждый поток создаётся свой контекст исполнения, включая память для стека, которая имеет минимальный размер в 4 КБ. Эту проблему может решить асинхронное программирование.

Асинхронность

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

Callbacks

Для написания асинхронной программы можно использовать callback-функции (от англ. callback — обратный вызов) — функции, которые будут вызваны асинхронно каким-либо обработчиком событий после завершения задачи. Переписанный пример сервера на callback-функциях:

В wait_connection() мы всё ещё ждём чего-то, но теперь вместе с этим внутри функции wait_connection() может быть реализовано подобие планировщика ОС, но с callback-функциями (пока мы ждём нового соединения, почему бы не обработать старые? Например, через очередь). Callback-функция вызывается, если в сокете появились новые данные — лямбда в async_read() , либо данные были записаны — лямбда в async_write() .

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

У такого подхода есть несколько проблем. Первую в шутку называют callback hell. Достаточно погуглить картинки на эту тему, чтобы понять, насколько это нечитаемо и некрасиво. В нашем примере всего две вложенные callback-функции, но их может быть намного больше.

Вторая проблема заключается в том, что код перестал выглядеть как синхронный: появились «прыжки» из wait_connection() в лямбды, например лямбда, переданная в async_write() , что нарушает последовательность кода, из-за чего становится невозможно предсказать, в каком порядке будут вызваны лямбды. Это усложняет чтение и понимание кода.

Async/Await

Попробуем сделать асинхронный код так, чтобы он выглядел как синхронный. Для большего понимания немного поменяем задачу: теперь нам необходимо прочитать данные из СУБД и файла по ключу, переданному по сети, и отправить результат обратно по сети.

Пройдём по программе построчно:

  • Ключевое слово async в заголовке функции говорит компилятору, что функция асинхронная и её нужно компилировать по-другому. Каким именно образом он будет это делать, написано ниже.
  • Первые три строки функции: создание и ожидание соединения.
  • Следующая строка делает асинхронное чтение, не прерывая основной поток исполнения.
  • Следующие две строки делают асинхронный запрос в базу данных и чтение файла. Оператор await приостанавливает текущую функцию, пока не завершится выполнение асинхронной задачи чтения из БД и файла.
  • В последних строках производится асинхронная запись в сокет, но лишь после того, как мы дождёмся асинхронного чтения из БД и файла.

Это быстрее, чем последовательное ожидание сначала БД, затем файла. Во многих реализациях производительность async / await лучше, чем у классических callback-функций, при этом такой код читается как синхронный.

Корутины

Описанный выше механизм называется сопрограммой. Часто можно услышать вариант «корутина» (от англ. coroutine — сопрограмма).

Далее будут описаны различные виды и способы организации сопрограмм.

Несколько точек входа

По сути корутинами называются функции, имеющие несколько точек входа и выхода. У обычных функций есть только одна точка входа и несколько точек выхода. Если вернуться к примеру выше, то первой точкой входа будет сам вызов функции оператором asynс , затем функция прервёт своё выполнение вместо ожидания БД или файла. Все последующие await будут не запускать функцию заново, а продолжать её исполнение в точке предыдущего прерывания. Да, во многих языках в корутине может быть несколько await ’ов.

Для большего понимания рассмотрим код на языке Python:

Программа выведет всю последовательность чисел факториала с номерами от 0 до 41.

Функция async_factorial() вернёт объект-генератор, который можно передать в функцию next() , а она продолжит выполнение корутины до следующего оператора yield с сохранением состояния всех локальных переменных функции. Функция next() возвращает то, что передаёт оператор yield внутри корутины. Таким образом, функция async_factorial() в теории имеет несколько точек входа и выхода.

Stackful и Stackless

В зависимости от использования стека корутины делятся на stackful, где каждая из корутин имеет свой стек, и stackless, где все локальные переменные функции сохраняются в специальном объекте.

Так как в корутинах мы можем в любом месте поставить оператор yield , нам необходимо где-то сохранять весь контекст функции, который включает в себя фрейм на стеке (локальные переменные) и прочую метаинформацию. Это можно сделать, например, полной подменой стека, как это делается в stackful корутинах.

На рисунке ниже вызов async создаёт новый стек-фрейм и переключает исполнение потока на него. Это практически новый поток, только исполняться он будет асинхронно с основным.

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

Наличие собственного стека позволяет делать yield из вложенных вызовов функций, но такие вызовы сопровождаются полным созданием/сменой контекста исполнения программы, что медленней, чем stackless корутины.

Более производительными, но вместе с тем и более ограниченными, являются stackless корутины. Они не используют стек, и компилятор преобразует функцию, содержащую корутины, в конечный автомат без корутин. Например, код:

Будет преобразован в следующий псевдокод:

По сути здесь создаётся класс, который сохраняет всё состояние функции, а также последнюю точку вызова yield . У такого подхода есть проблема: yield может быть вызван только в теле функции-корутины, но не из вложенных функций.

Симметричные и асимметричные

Корутины также делятся на симметричные и асимметричные.

Симметричные имеют глобальный планировщик корутин, который и выбирает среди всех ожидающих асинхронных операций ту, которую стоит выполнить следующей. Примером является планировщик, о котором говорилось в начале функции wait_connection() .

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

Вывод

Асинхронное программирование является очень мощным инструментом для оптимизации высоконагруженных программ с частым ожиданием системы. Но, как и любую сложную технологию, её нельзя использовать только потому, что она есть. Необходимо всегда задавать себе вопрос: а нужна ли мне эта технология? Какую практическую пользу она мне даст? Иначе разработчики рискуют потратить очень много сил, времени и денег, не получив никакого профита.

Асинхронное программирование. Часть 1: Как работает процессор

Процессоры могут выполнять программы асинхронно. Объясняем, как это происходит, зачем нужно и что значит для программирования.

Асинхронность, многопоточность и параллелизм — очень важные аспекты программирования, без которых сложно представить современные компьютеры.

Читать еще:  Язык программирования c 7

Язык программирования C# предоставляет разработчикам множество инструментов для создания приложений, которые могут работать с большим количеством потоков, выполняя их параллельно и асинхронно. Это одна из причин популярности этого языка, а также главная причина, почему именно он выбран для этой серии статей.

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

ВНИМАНИЕ!

Информация в следующих разделах сильно упрощена.

Евгений Кучерявый

Пишет о программировании, в свободное время создает игры. Мечтает открыть свою студию и выпускать ламповые RPG.

Как работает процессор

За единицу времени (она называется тик) процессор может выполнить только одну задачу — например, прочитать значение ячейки памяти. Поэтому на то, чтобы выполнить следующие операции, потребуется 4 тика:

  1. Прочитать данные из двух ячеек.
  2. Сложить их.
  3. Записать результат в другую ячейку.

Это касается только атомарных операций, то есть самых маленьких и неделимых. Более сложные задачи могут состоять из нескольких атомарных. Например, чтобы провести умножение числа A на число B, нужно будет прибавить к числу A само себя B — 1 раз.

5 * 5 = 5 + 5 + 5 + 5 + 5

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

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

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

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

Количество тиков измеряется в герцах (Гц) — это единица измерения частоты протекания периодических процессов. Например, если мимо вашего дома раз в секунду проезжает гоночный болид, то его частота будет равна 1 Гц. Если болид проезжает два раза в секунду, то его частота — 2 Гц; если он проезжает трижды, то давно пора подумать о переезде.

Процессор так быстро выполняет процессы, что его частота измеряется в гигагерцах.

1 ГГц = 1 000 000 000 Гц

Частота современных процессоров обычно равна 2-3 ГГц.

Как процессор выполняет программы

Каждая программа состоит из множества процессов: нужно 500 раз провести сложение, записать данные в 2000 ячеек, перемножить всё это, а потом, наконец, поделить.

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

Тогда код программы будет выглядеть примерно так:

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

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

Тогда каждый поток получит по 10 тиков. То есть процессор будет в течение 10 тиков выполнять инструкции от одного потока, а потом переходить к инструкциям другого — и так по кругу. Также у каждого потока есть приоритет: более важные программы будут получать больше тиков.

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

Когда нужна асинхронность

Чаще всего асинхронность нужна в программах с графическим интерфейсом. В них основная логика и работа с изображением помещены в разные потоки. Поэтому, когда логика занята, вы всё ещё можете пользоваться приложением.

Если же всё это выполняется в одном потоке, то приложение будет подвисать, когда выполняется сложная инструкция. В ОС Windows часто можно заметить, что, когда приложение что-то делает, а вы кликаете на него, то в заголовке окна можно увидеть словосочетание «Не отвечает».

Это не всегда означает, что приложение зависло. Возможно, оно просто занято решением какой-то сложной задачи, которая выполняется в том же потоке.

Пример асинхронного приложения

Чтобы лучше закрепить тему, давайте напишем приложение с использованием асинхронности. Оно будет разделено на два потока (Thread): первый будет обновлять полосу загрузки, а второй — выполнять саму загрузку.

В следующих статьях вы узнаете, как всё устроено, а пока посмотрим, как это работает:

Здесь можно заметить, что курсор обновляется чаще (каждые 100 мс), чем проценты (каждые 500 мс), как и было записано в коде программы.

Заключение

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

Учитывая сложность этих инструментов, понадобится не одна статья, чтобы их объяснить. Я постараюсь не только показать, как их использовать, но и помочь на более глубоком уровне понять принципы их работы.

Многое об АМП вы можете узнать на нашем курсе «Профессия C#-разработчик». Там вы освоите все инструменты, необходимые программисту на C#, чтобы постепенно стать профессионалом.

namespace sansys.net <

public class Blog: ISite <
// TODO: Сделать мир лучше
>

04 мая 2013 by Alexander Pascal

Асинхронное программирование в C# используя async/await

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

Для обеспечения работы UI потенциально длительные операции должны быть выполнены асинхронно, в отдельных потоках. Здесь ничего нового, но многие приложения до сих пор выполняют операции в UI-потоке. Почему? Одна из причин в том, что асинхронное программирование неудобно. Это гораздо более сложнее, тяжелее писать, тестировать, поддерживать, и если не написать должным образом это может привести к снижению производительности и блокировкам.

На протяжении многих лет версии .NET предлагали постепенно совершенствующиеся модели которые пытаются уменьшить сложность асинхронного программирования.
Но так было до .NET 4.5 / C# 5 — наконец мы можем писать асинхронный код так же просто, как и обыкновенный, а компилятор уже сам сделает всю тяжелую работу за нас (страшновато звучит. ). По факту асинхронный код может быть почти идентичным его синхронному аналогу. Теперь без лишних слов давайте рассмотрим примеры async/await

Введение в acync/await

Теперь представьте, что интернет медленный и скачивание картинки занимает 30 секунд. На протяжении этого времени приложение не будет отвечать. Если пользователь нетерпелив, он может остановить работу приложения до окончания загрузки изображения. В общем это не очень хороший подход. Мы должны загружать изображение асинхронно. Давайте посмотрим как мы можем легко сделать это используя async/await:

Читать еще:  Знак деления в программировании

Этот код почти идентичен! В представленном коде три отличия: метод обозначен ключевым словом async, вызову загрузки картинки предшествует await и картинка загружается методом DownloadDataTaskAsync, являющимся асинхронным аналогом метода DownloadData. Давайте рассмотрим каждое изменение более подробно.

Ключевое слово async говорит компилятору, что метод может содержать ключевое слово await. Это необходимо для обратной совместимости с кодом, написанным до введения await в C# 5, где await можно было использовать в качестве идентификатора. (т.е. если метод не обозначить как async, то await можно использовать в качестве имени переменной, иначе — нельзя) . Это также позволяет компилятору быть уверенным в том, что мы используем await внутри метода, и выдавать исключение, если мы забыли сделать это.

Метод DownloadData класса Webclient загружает данные синхронно, до возвращения управления вызывающему. Но нам нужно, чтобы метод возвращал управление немедленно и выполнял загрузку асинхронно. Многие .NET FCL классы сегодня предлагают асинхронные версии своих методов, где это, конечно, возможно. Как правило асинхронные методы, к коим можно применить await, именуются как xxxAsync. Однако в случае класса WebClient там уже был метод DownloadDataAsync до .NET 4.5, так что был добавлен метод DownloadDataTaskAsync. Если вы сомневаетесь, какой метод использовать с await, то задпомните одно: await может быть использован только с методами возлвращающими Task или Task .

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

Использование await

В примере выше мы использовали await однажды, но мы можем использовать его несколько раз в одном методе. Например, мы можем скачивать несколько изображений. Это значит, что у метода будет несколько возвратов и восстановлений.

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

Правила и ограничения

В то время как использование async/await очень гибко, оно омеет некоторые ограничения. Наиболее заметные из них:

  • await не может быть использован внутри блоков catch/finally
  • Конструкторы и аксессоры свойств не могут быть помечены как async и использовать await
  • async метод не может иметь ref/out параметры
  • await не может быть использован внутри блока lock

Управление исключениями

Асинхронное программирование в C# 5 было устроено как можно безболезненее и управление исключениями не исключение (каламбур). По факту мы пишем код управления исключениями точно так же как и в синхронном варианте кода:

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

Один интересный вопрос, что происходит с finnaly блоками? Рассмотрим следующий код:

finnaly блоки гарантированно выполняются, когда управление покидает соответствующие try/catch блоки. в результате, это также гарантирует выполнение если метод возвращает результат из try/catch блока. Как мы уже знаем, await делает возврат. Так что же произойдет в коде выше? Будет ли код в finally выполняться когда возвращается выолнение в await или будет выполнен только после восстановление метода и когда код логически покинет код try?

Всякий раз, когда вы задаете себе подобные вопросы, полезно иметь ввиду, что поддержка асинхронности в C# 5 (?) была разработана чтобы чувствовать себя естественно, как в синхронном коде (по возможности). В асинхронном коде происходит то, что в общем происходило бы и в синхронном коде. Если бы код выше был синхронным, вы бы ожидали выполнение finnaly-блока после завершения блока try (при условии отсутствия исключений). Такое же поведение применимо и к асинхронному коду. Даже, если технически метод возвращает результат await, логически при await ничего еще не вернулось. Следовательно finally-блок в коде выше выполняется когда код try-блока завершится (или пробросится исключение), собтвенно как и в синхронном варианте.

Как это работает?

C# 5 позволяет писать асинхронный код используя упрощенный синтаксис, и оставлять грязную работу компилятору. За кулисами, когда асинхронный метод скомпилирован, JIT генерирует машинный код. Примерно так это работает, если не вдаваться в подробности.

Машинный код имеет метод, который содержит код оригинального метода, но он разбит на сегменты встроенные в switch-блоки. Когда метод выполняется — проверяется состояние и запускается соответствующий сегмент кода. При первом выполнении метода, состояние имеет какое-то начальное значение, следовательно выполнение начинается с оригинального метода. Напомню, что await применим только к методам типа Task/Task . Когда мы ждем метод, Task-объект используется для регистрации метода обратного вызова, который будет вызван при завершении асинхронной операции. Когда это происходит, метод выполняется снова. Но в этот раз с другим состоянием, так что метод выполняет другой сегмент оригинального кода, который, скорее всего, будет расположен за await-сегментом.

Это упрощенная версия описания того, как это работает.

Итого

C# 5 и .NET 4.5 огромный шаг вперед в упрощении асинхронного программирования и почти удачный. С введением новых ключевых слов (в C#) и многих асинхронных версий методов в FCL асинхронный код приближен к синхронному. Это также делает преобразование синхронного кода в асинхронный проще, что способствует отзывчивости приложений и счастью пользователей.

Немного абстрактный пример:

Хочется заметить, что создавать таску в методе AsyncHello не так уж обязательно, следующее определение метода также будет работать, да и читать проще:

Синхронная асинхронность в C++

Наверняка все, кто изучал старый добрый стандарт C++11, знают о существовании в стандартной библиотеке вызова std::async, который позволяет выполнить некий код асинхронно (более точно – поведение указывается первым параметром вызова).

Согласно документации, вызов с параметром std::launch::async обещает выполнить пользовательский код в отдельном потоке. Посмотрим на приведённый ниже код.

В строках 8-13 запускаем асинхронное выполнение простой lambda-функции, которая должна вывести на экран цифру «1» каждую миллисекунду десять раз. В строках 14-19 запускаем выполнение аналогичной функции, но на этот раз она будет выводить на экран цифру «2». Что можно ожидать на экране по окончанию выполнения программы?

Кто сказал, что «результат не определён»?

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

Звучит логично, но эта гипотеза неверна. На самом деле на экран гарантированно будет выведена последовательность:

Почему? Что произошло?

А произошла принудительная синхронизация двух потоков. Выполнение второго потока (с выводом цифры «2») гарантированно начнётся только после того, как первый поток закончит своё выполнение.

Кто догадается, почему?

На самом деле не всё так просто. Но достаточно задуматься, про что мы забыли в этом примере? А забыли мы про то, что в качестве результата вызов std::async возвращает std::future. Если бы мы написали наш пример следующим образом, то результат на экране стал бы действительно неопределённым:

Читать еще:  Язык программирования q

Вот теперь на экране действительно может быть любая последовательность из перемешанных двадцати цифр 1 и 2. Почему результат так кардинально изменился, стоило нам только лишь сохранить std::future, которое вернул вызов std::async?

Как говорится, всё законно, всё по стандарту

Стандарт гарантирует, что окончание выполнение потока, запущенного вызовом std::async, синхронизировано с вызовом получения результата std::future::get или с освобождением общего состояния (shared state) – области памяти, ответственной за передачу результата между std::async и std::future.

В первом примере автоматическое удаление временного объекта std::future, который был возвращён из первого вызова std::async, приводит к освобождению общего состояния и автоматической синхронизации двух потоков. Просто не сохранив результат вызова std::async, мы получили ожидание – второй поток не начнёт выполнение до окончания выполнения первого потока.

Есть вопрос? Напишите в комментариях!

Асинхронное программирование в C#. Вступление.

8 октября 2018 г.

Многие слышали об этом, но не многие используют его в своем коде. Между тем, никакие более-менее серьезные программы с клиент-серверной архитектурой не обойдутся без асинхронного программирования. Обмен данными с базой, взаимодействие клиента и сервера – это требует времени, которое можно занять другими процессами вместо ожидания.

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

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

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

То есть, ожидая ответа от другой задачи, текущая не блокирует поток, а предоставляет его другой задаче.

Давайте посмотрим на рисунок:

В этом случае используется однопоточный асинхронный подход.

А здесь многопоточный асинхронный поток.

Каждый поток выполняет множество задач. Когда одна из задач останавливается в ожидании, берется другая. Таким образом задачи перетекают от одного потока к другому, в зависимости от того, который освободился первым. Из рисунка видно, что Task 1 начала выполняться в первом потоке, а закончила во втором.

Рассмотрим еще один рисунок – диаграмма последовательности:

Диаграмма описывает поведение потоков для клиент-серверного приложения. Клиент отправляет запрос данных от сервера, и, вместо того чтобы «зависнуть» ожидая ответ (как в синхронном подходе) продолжает работать, предоставляя пользователю другой функционал приложения. Так что, если вы веб разработчик, то без асинхронности – никуда.

Давайте немного разберемся в теории. Всего можно выделить три шаблона асинхронного программирования:

  • Asynchronous Programming Model (APM);
  • Event-based Asynchronous Pattern (EAP);
  • Task-based Asynchronous Pattern(TAP).

Asynchronous Programming Model появился еще в первой версии .Net Framework. APM позволил создавать асинхронные версии синхронных методов посредством двух методов — Begin и End .

Итак, всего два метода:

Метод Begin начинает асинхронную операцию. Он принимает параметры args, callback – делегат на метод, вызываемый после выполнения асинхронного метода, объект userState, который используется для передачи информации о состоянии конкретного приложения в метод, вызываемый при завершении асинхронной операции.

Метод возвращает объект типа IAsyncResult который хранит информацию об асинхронной операции.

Метод End завершает асинхронную операцию. Он принимает на вход объект, типа IAsyncResult, а возвращает TResult, который на самом деле вернет тип, определенный в синхронной копии этого метода.

Давайте посмотрим, как используется этот шаблон на упрощенном примере:

Мы вызвали метод Begin в обработчике события нажатия кнопки. В качестве параметра передаем callback в этот метод. И, уже в самом callback-е вызываем парный метод – End.

К недостаткам асинхронной модели программирования можно отнести:

  • Необходимо создавать функцию обратного вызова;
  • Нет возможности прервать операцию.
  • Нет возможности отказаться от вызова метода обратного вызова, если он был передан при начале операции.
  • Нет оповещения о прогрессе операции или о промежуточных результатах.

Eventbased Asynchronous Pattern

Этот шаблон асинхронного программирования появился во второй версии фреймворка .Net. Он основан на событиях и асинхронных методах. Класс, реализующий этот шаблон будет содержать методы MethodNameAsync и MethodNameAsyncCancel (если нужна обработка отмены операции), и событие MethodNameCompleted. В этом же классе можно разместить синхронные версии методов, работающие с тем же потоком. Чаще всего этот шаблон используют при работе с веб сервисами. Например, ajax реализует Event-based Asynchronous Pattern. Получить результат асинхронной операции и обработать ошибки можно только в обработчике события MethodNameCompleted.

Шаблон асинхронного программирования, основанный на событиях решил некоторые проблемы предшественника:

  • Объявление метода для получения результата асинхронной операции;
  • Не предусмотрен механизм оповещения о прогрессе операции.

Однако, данный шаблон все равно имеет ряд недостатков:

  • Нет возможности передать контекст вызова операции (пользовательские данные) в метод обработки результата;
  • Не все операции можно прерывать. Методы, поддерживающие только одну операцию в очереди невозможно прервать;
  • Невозможно задать, в контексте какого потока будут вызываться методы обратного вызова.

Task-based Asynchronous Pattern (TAP)

Третий шаблон асинхронного программирования появился в .Net Framework 4.0. Из названия понятно, что он базируется на использовании задач. Основа TAP — два типа System.Threading.Tasks.Task и System.Threading.Tasks.Task

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

TAP использует задачи для выполнения операций. Для каждой задачи используется отдельный поток, который берется из пула потоков. После выполнения задачи, поток возвращается в пул.

Модификатор «async» — этот модификатор применяется к методу или лямбда-выражению, или анонимному методу — он указывает, что метод является асинхронным и сигнализирует о возможности одного или нескольких вхождений оператора ожидания в этот метод.

Давайте посмотрим на пример определения метода:

Обратите внимание на ключевые слова async и await. Именно эти операторы сигнализируют, что используется Task-based Asynchronous Pattern. Модификатор async указывает, что метод асинхронный. А оператор await может быть вызван внутри метода один или несколько раз. Он приостанавливает выполнение задачи до получения результата, в то время как поток продолжает свою работу.

А вот пример исползования TAP из жизни. Вызов веб службы:

А вот другой способ вызова, еще более простой и понятный:

Этот «облегченный» вариант использования await стал доступен в .Net Framework 4.5.

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

На текущий момент Microsoft рекомендует использовать именно этот шаблон для реализации асинхронных вызовов при разработке компонентов.

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

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

Ссылка на основную публикацию
ВсеИнструменты 220 Вольт
Adblock
detector
×
×