Light-electric.com

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

Асинхронный язык программирования

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

    Статьи, 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 создаёт новый стек-фрейм и переключает исполнение потока на него. Это практически новый поток, только исполняться он будет асинхронно с основным.

Читать еще:  Основы программирования visual basic

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

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

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

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

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

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

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

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

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

Вывод

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

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

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

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

Концепция асинхронного программирования

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

Локальные методы, вероятность исключений которых сведена к минимуму, не нуждаются в асинхронном подходе (например, изменение цвета шрифта текста или его размера), но другие методы (ожидание чтения файла или запуск web-службы) требуют его в самом начале разработки. Среда Common Language Runtime поддерживает достаточно широкое количество методов и классов асинхронного программирования.

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

Когда мы запускаем «долгий» метод, процессор будет выполнять его, при этом ему будет «некогда» выполнять другие запросы пользователя (рис. 7.2).

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

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

Поддержка асинхронного программирования в .NET Framework

Классы, в которых есть встроенная поддержка асинхронной модели , имеют пару асинхронных методов для каждого из синхронных методов. Эти методы начинаются со слов Begin и End. Например, если мы хотим воспользоваться асинхронным вариантом метода Read класса System.IO.Stream, нам нужно использовать методы BeginRead и EndRead этого же класса.

Для использования встроенной поддержки асинхронной модели программирования нужно вызвать соответствующий метод BeginOperation и выбрать модель завершения вызова. Вызов метода BeginOperation возвращает объект интерфейса IAsyncResult , с помощью которого определяется состояние выполнения асинхронной операции. Завершить выполнение асинхронных методов можно несколькими способами.

Метод EndOperation

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

Для завершения выполнения параллельного потока ar здесь был вызван метод EndRead . В качестве кода, имитирующего длительную работу, можно подключить точный счетчик выполнения задачи, разобранный нами в «Использование библиотек кода в windows-формах» .

Способ опроса (Pooling)

Этот способ подходит в случаях, когда требуется контролировать выполнение асинхронного метода. При его использовании приходится осуществлять проверку выполнения асинхронного метода вручную, с помощью свойства IsCompleted объекта типа IAsyncResult . Это не самая распространенная техника завершения, поскольку большинство процессов не требует контроля выполнения. Пример использования способа опроса:

Читать еще:  С язык программирования для чайников

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

Способ Callback

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

Пример использования варианта Callback :

На диске, прилагаемом к книге, вы найдете приложения EndOperation , Pooling и Callback (CodeGlava7 EndOperation, Pooling, Callback).

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

Node Hero: Глава 3

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

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

В традиционной практике программирования большинство операций ввода-вывода происходит синхронно. Если вы задумайтесь о Java и о том, как вы читаете файл в ней, вы получите что-то вроде этого:

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

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

Для решения этой проблемы Node.js предлагает модель асинхронного программирования.

Асинхронное программирование в Node.js

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

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

Начнем с простого примера: синхронное чтение файла с использованием Node.js:

Что здесь происходит? Мы читаем файл, используя синхронный интерфейс модуля fs . Он работает ожидаемым образом: в переменную content сохраняется содержимое file.md . Проблема с этим подходом заключается в том, что Node.js будет заблокирована до завершения операции, то есть, пока читается файл, она не может сделать ничего полезного.

Посмотрим, как мы можем это исправить!

Асинхронное программирование, в том виде, в каком мы знаем его в JavaScript, может быть реализовано только при условии, что функции являются объектами первого класса: они могут передаваться как любые другие переменные другим функциям. Функции, которые могут принимать другие функции в качестве аргументов, называются функциями высшего порядка.

Один из самых простых примеров функций высшего порядка:

В приведенном выше примере мы передаем функцию isBiggerThanTwo в функцию filter . Таким образом, мы можем определить логику фильтрации.

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

В основе Node.js лежит принцип «первым аргументом в колбеке должна быть ошибка». Его придерживаются базовые модули, а также большинство модулей, найденных в NPM.

Что следует здесь выделить:

  • обработка ошибок: вместо блока try-catch вы проверяете ошибку в колбеке
  • отсутствует возвращаемое значение: асинхронные функции не возвращают значения, но значения будут переданы в колбеки

Давайте немного изменим этот файл, чтобы увидеть, как это работает на практике:

Результатом выполнения этого кода будет:

Как вы можете видеть, как только мы начали читать наш файл, выполнение кода продолжилось, а приложение вывело end of the file . Наш колбек вызвался только после завершения чтения файла. Как такое возможно? Встречайте цикл событий (event loop).

Цикл событий

Цикл событий лежит в основе Node.js и JavaScript и отвечает за планирование асинхронных операций.

Прежде чем погрузиться глубже, давайте убедимся, что мы понимаем, что такое программирование с управлением по событиям (event-driven programming).

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

На практике это означает, что приложения реагируют на события.

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

Для более глубокого понимания работы цикла событий рекомендуем посмотреть это видео:

Асинхронный поток управления

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

Async.js

Чтобы избежать так называемого Callback-Hell, вы можете начать использовать async.js.

Async.js помогает структурировать ваши приложения и упрощает понимание потока управления.

Давайте рассмотрим короткий пример использования Async.js, а затем перепишем его с помощью промисов.

Следующий фрагмент перебирает три файла и выводит системную информацию по каждому:

Примечание переводчика: если вы пользуетесь Node.js версии 7 и выше, лучше воспользоваться встроенными конструкциями языка, такими как async/await.

Промисы

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

На практике предыдущий пример можно переписать следующим образом:

Конечно, если вы используете метод, возвращающий промис, то пример будет заметно компактнее.

Основы Dart 2.x (асинхронное программирование)

Введение:

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

  • Асинхронность (asynchrony) подразумевает, что операция может быть выполнена кем-то на стороне: удаленным веб-узлом, сервером или другим устройством за пределами текущего вычислительного устройства при этом не блокируя главный поток выполнения программы.
  • Асинхронность в программировании — выполнение процесса в неблокирующем режиме системного вызова, что позволяет потоку программы продолжить обработку. Проще говоря, главный «процесс» ставит задачу и передает ее другому независимому «процессу», при этом сам продолжает работать.
Читать еще:  Имплементация в программировании

Например: Вы решили купить новый аккумулятор на свою «Приору», на сайте или в мобильном приложении в фильтре товаров, выбираете аккумулятор по необходимым вам критериям (Производитель, Ёмкость, Сила тока холодной прокрутки) и после нажатия кнопки «найти товар по заданным критериям» выполняется асинхронная операция поиска на сервере необходимого вам товара, во время этого поиска, вы можете скролить сайт, написать в чат менеджеру магазина, вообщем выполнять ещё какие либо действия пока происходит поиск товара. Если бы операция выполнялась синхронно, вы бы не смогли взаимодействовать с приложением пока не завершится поиск и вывод на страницу товара.

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

DOC:dart.dev
Код в Dart работает в одном треде (потоке) выполнения. Если код занят долгими вычислениями или ожидает операцию ввода/вывода, то вся программа приостанавливается.
Dart использует futures для представления результатов асинхронных операций. Для работы с futures можно использовать async и await или Future API.

future — объект класса Future , который представляет собой асинхронную операцию, возвращающую результат типа T. Если результат операции не используются, то тип future указывают Future . При вызове функции, возвращающей future, происходит две вещи:

    1. Функция встает в очередь на выполнение и возвращает незавершенный объект Future.
    1. Когда операция завершена, future завершается со значением или ошибкой.
  • Для написания кода, зависящего от future, у вас есть два варианта:

    • a) Использовать async — await
    • b) Использовать Future API

    async — await

    Ключевые слова async и await являются частью поддержки асинхронности в Dart. Они позволяют писать асинхронный код, который выглядит как синхронный код и не использует Future API.

    Асинхронная функция — это функция, перед телом которой находится ключевое слово async. Ключевое слово await работает только в асинхронных функциях.

    Future API

    • Если писать асинхронный код с помощью Future API, используется метод then() для регистрации обратного вызова. Этот обратный вызов сработает, когда future завершится.

    Метод printDailyNewsDigest() использует readAsString(), который не будет блокировать выполнение программы. Вызов readAsString() будет стоять в очереди на выполнение и не будет останавливать работу остальной части кода. Программа печатает номера лотереи, прогноз погоды и футбольный счет. Когда readAsString() заканчивает чтение файла новостей, программа печатает его содержимое. Если readAsString() занимает некоторое время, чтобы завершить свою работу, никой проблемы в этом не будет: пользователь может прочитать другие материалы до того, как будет напечатан ежедневный дайджест новостей.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    0 0 голоса
    Рейтинг статьи
    Ссылка на основную публикацию
    ВсеИнструменты