Что такое асинхронность в программировании? [дубликат]
Не понимаю, почему подход называется асинхронным, если по сути, выполнение этих функций просто переносится в конец синхронной очереди (event loop). Тоже самое, если их просто вызвать последними? Я думал, что это когда параллельно выполняются задачи в одном потоке.
Отслеживать
12.9k 10 10 золотых знаков 40 40 серебряных знаков 76 76 бронзовых знаков
задан 22 янв 2021 в 4:20
Rouslan Kartoev Rouslan Kartoev
31 3 3 бронзовых знака
Синхронный код выполняется строго сверху вниз: Пока одня строчка кода не выполнится, следующая строчка не будет выполнена. Асинхронный код позволяет откладывать выполнение некоторых фрагментов кода, и пока они «ждут», можно продолжать выполнение синхронного кода, не теряя времени. Задачи не выполняются параллельно в одном потоке. Т.е. суть асинхронности: Последовательность строчек кода и последовательность их выполнения не обязательно будет совпадать.
22 янв 2021 в 4:27
Поймите простую истину. Асинхронность подразумевает одну вещь. Я вызвал но ответ мне сейчас прям не нужен, когда прийдёт тогда и посмотрим. Тем самым вы не блокируете единственный в случае JavaScript (в многопоточных средах это не так) поток. Это как вы директор и вызвали к себе сотрудника, если ожидать его то это синхронно, а если вызвали и делаете свои дела когда прийдёт тогда и поговорим то это асинхронно
22 янв 2021 в 4:36
Вы говорите что «ждет» Ну так я не понимаю, как тогда работает например js когда файл например обрабатывается, а я паралелльно свободно могу управлять ui.
22 янв 2021 в 6:16
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Стоит объяснить на примере. В одном из обработчиков мы отправили запрос к БД. БД его обрабатывает 5 секунд.
app.get('/syncRequestForTest', function(req, res)< // сервер может принять и обработать один запрос в 5 секунд. let user = yourDB.users.findSync() // у нас очень много данных БД, поэтому БД обрабатывает такой запрос 5 секунд. req.send(user) > app.get('/other', function(req, res) < // сервер не может обработать этот запрос, пока он занят синхронными вычислениями. . >
В данном примере программа будет ждать синхронный запрос 5 секунд, пока БД вернет ответ. Эти 5 секунд она не может обрабатывать другие запросы, что недопустимо в продакшене.
Если же запрос будет выполняться асинхронно, то во время ожидания ответа от БД сервер сможет обрабатывать другие запросы:
app.get('/asyncRequestForTest', async function(req, res)< // сервер может принимать сотни таких запросов в секунду. let user = await yourDB.users.findAsync() // у нас очень много данных БД, поэтому БД обрабатывает такой запрос 5 секунд. req.send(user) > app.get('/other', function(req, res) < // сервер может обработать этот запрос, пока где-то снаружи основного потока приложения выполняются асинхронные задачи. . >
Обычно асинхронный подход используется для запросов к сторонним программам (fetch-запрос в браузере, чтение и запись файлов, запросы к БД). Потому что эти запросы обычно длятся сравнительно долго и при этом не загружают ресурсы (все что делает приложение — ждет), а приостанавливать работу всего приложения (например, сервера), пока читается файл для обработки одного из запросов, недопустимо.
Вот эта статья на английском с картинками описывает разницу между синхронным, асинхронным выполнением, а также касается многопоточности.
Вот статья на Хабре с объяснением разницы между этими подходами.
Вот вопрос на русском StackOverflow, где много хороших примеров по разнице между многопоточностью, асинхронностью, синхронностью и конкуррентностью.
Продублирую здесь ответ:
Многопоточное программирование подразумевает, что код приложения выполняется в разных потоках. Например, есть главный поток UI, и несколько рабочих потоков, которые выполняют тяжелые вычисления, результаты которых затем выводятся на UI.
Асинхронное программирование подразумевает инициацию некоторой операцию, об окончании которой главный поток узнает спустя некоторое время. Обычно это применяется для работы с системой ввода-вывода: диски, сеть и т.д. При этом, если это все сделано правильно, никакого потока нет. Также часто под выражением «выполнить асинхронно» подразумевают, что выполнение некоторого кода будет произведено не в текущем потоке, а в соседнем, при этом текущий поток не будет заблокирован. Но мой взгляд, это не совсем корректно.
Параллельное программирование подразумевает разбиение одной задачи на независимые подзадачи, которые можно рассчитать параллельно, а затем объединить результаты. Один из примеров — это map-reduce. Это частный случай многопоточного программирования.
Асинхронное программирование
Асинхронное программирование — концепция программирования, при которой результат выполнения функции доступен спустя некоторое время в виде асинхронного (нарушающего стандартный порядок выполнения) вызова. Запуск длительных операций происходит без ожидания их завершения и не блокирует дальнейшее выполнение программы.

«IT-специалист с нуля» наш лучший курс для старта в IT

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

Использование принципов асинхронного программирования повышает производительность, отзывчивость и стабильность приложений, которые отвечают следующим характеристикам:
- Непрерывно выполняют большое количество заданий — среди множества операций всегда будет та, которая должна работать, когда остальные блокируются.
- Задания выполняют множество операций по вводу и выводу информации, заставляя синхронную программу впустую тратить время на блокировки.
- Задания в основном независимы друг от друга, и необходимость обмена данными между операциями практически отсутствует.
Перечисленные условия отличают рабочий процесс серверного приложения, отвечающего на запросы клиента. Поэтому в последние годы популярны серверные платформы, способные работать в асинхронном режиме, такие как Node.js.
Что такое многопоточность
Многопоточность — это параллельное выполнение нескольких блоков (потоков) приложения независимо друг от друга. Например, платформа Node.js использует два вида потоков.
Основной. Обрабатывается циклом событий.
Вспомогательные. Обрабатываются в пуле воркеров — модуле для произведения операций ввода-вывода, которые особенно интенсивно нагружают процессор. Такие операции обычно возникают при обработке сетевых запросов и взаимодействии с системным диском.

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

Проиллюстрируем блокировку на примере исполнения кода:
const btn = document.querySelector(‘button’);
btn.addEventListener(‘click’, () => let myDate;
for(let i = 0; i < 5000000; i++) let date = new Date();
myDate = date
>
console.log(myDate);
let pElem = document.createElement(‘p’);
pElem.textContent = ‘Это новый параграф.’;
document.body.appendChild(pElem);
>);
При нажатии на кнопку запускается операция по расчету и выводу на консоль 5 000 000 дат. Новый параграф на странице появится только после завершения операции. Если же вместо расчета дат скрипт будет выполнять более сложные операции, браузер, скорее всего, на несколько минут зависнет.
Это происходит потому, что JavaScript — однопоточный язык. Он выполняет все задачи (потоки) последовательно. Некоторые программные платформы, например языки семейства .NET или Go, создавались с расчетом на асинхронное программирование и многопоточную обработку данных. Однако JavaScript разрабатывался для выполнения простых действий вроде обработки нажатий кнопок на веб-странице. Со временем разработчики создали несколько способов реализовать асинхронную обработку данных в JavaScript-приложениях.
Профессия / 9 месяцев
Frontend-разработчик
Создавайте интерфейсы сервисов, которыми пользуются все
Асинхронность в программировании
В высоконагруженных системах эффективная работа с помощью простых потоков затруднена. Разбираемся, как решить проблему с помощью асинхронности.
Традиционно в программировании используют синхронное программирование — последовательное выполнение инструкций с синхронными системными вызовами, которые полностью блокируют поток выполнения, пока системная операция, например чтение с диска, не завершится. В качестве примера ниже написан echo-сервер:
while (true) < std::string data; auto socket = Socket(localhost, port); socket.wait_connection(); while (!socket.end_of_connection()) < data = socket.read(); // Блокировка socket.write(data); // Блокировка >>
При вызове методов read() и write() текущий поток исполнения будет прерван в ожидании ввода-вывода по сети. Причём большую часть времени программа будет просто ждать. В высоконагруженных системах чаще всего так и происходит — почти всё время программа чего-то ждёт: диска, СУБД, сети, UI, в общем, какого-то внешнего, независимого от самой программы события. В малонагруженных системах это можно решить созданием нового потока для каждого блокирующего действия. Пока один поток спит, другой работает.
Но что делать, когда пользователей очень много? Если создавать на каждого хотя бы один поток, то производительность такого сервера резко упадёт из-за того, что контекст исполнения потока постоянно сменяется. Также на каждый поток создаётся свой контекст исполнения, включая память для стека, которая имеет минимальный размер в 4 КБ. Эту проблему может решить асинхронное программирование.
Асинхронность
Асинхронность в программировании — выполнение процесса в неблокирующем режиме системного вызова, что позволяет потоку программы продолжить обработку. Реализовать асинхронное программирование можно несколькими способами, о которых вы узнаете ниже.
Callbacks
Для написания асинхронной программы можно использовать callback-функции (от англ. callback — обратный вызов) — функции, которые будут вызваны асинхронно каким-либо обработчиком событий после завершения задачи. Переписанный пример сервера на callback-функциях:
while (true) < auto socket = Socket(localhost, port); socket.wait_connection(); // Всё ещё есть блокировка socket.async_read([](auto &data) /* * Поток не блокируется, лямбда-функция будет вызвана * каждый раз после получения новых данных из сокета, * а основной поток пойдёт создавать новый сокет и * ждать новое соединение. */ < socket.async_write(data, [](auto &socket) < if (socket.end_of_connection()) socket.close(); >); >); >
В wait_connection() мы всё ещё ждём чего-то, но теперь вместе с этим внутри функции wait_connection() может быть реализовано подобие планировщика ОС, но с callback-функциями (пока мы ждём нового соединения, почему бы не обработать старые? Например, через очередь). Callback-функция вызывается, если в сокете появились новые данные — лямбда в async_read() , либо данные были записаны — лямбда в async_write() .
В результате мы получили асинхронную работу нескольких соединений в одном единственном потоке, который намного реже будет ждать. Эту асинхронность можно также распараллелить, чтобы получить полный профит от утилизации процессорного времени.
У такого подхода есть несколько проблем. Первую в шутку называют callback hell. Достаточно погуглить картинки на эту тему, чтобы понять, насколько это нечитаемо и некрасиво. В нашем примере всего две вложенные callback-функции, но их может быть намного больше.
Вторая проблема заключается в том, что код перестал выглядеть как синхронный: появились «прыжки» из wait_connection() в лямбды, например лямбда, переданная в async_write() , что нарушает последовательность кода, из-за чего становится невозможно предсказать, в каком порядке будут вызваны лямбды. Это усложняет чтение и понимание кода.
Async/Await
Попробуем сделать асинхронный код так, чтобы он выглядел как синхронный. Для большего понимания немного поменяем задачу: теперь нам необходимо прочитать данные из СУБД и файла по ключу, переданному по сети, и отправить результат обратно по сети.
public async void work() < var db_conn = Db_connection(localhost); var socket = Socket(localhost, port); socket.wait_connection(); var data = socket.async_read(); var db_data = db_conn.async_get(await data); var file_data = File(await data).async_read(); await socket.async_write($””); socket.close(); >
Пройдём по программе построчно:
- Ключевое слово async в заголовке функции говорит компилятору, что функция асинхронная и её нужно компилировать по-другому. Каким именно образом он будет это делать, написано ниже.
- Первые три строки функции: создание и ожидание соединения.
- Следующая строка делает асинхронное чтение, не прерывая основной поток исполнения.
- Следующие две строки делают асинхронный запрос в базу данных и чтение файла. Оператор await приостанавливает текущую функцию, пока не завершится выполнение асинхронной задачи чтения из БД и файла.
- В последних строках производится асинхронная запись в сокет, но лишь после того, как мы дождёмся асинхронного чтения из БД и файла.
Это быстрее, чем последовательное ожидание сначала БД, затем файла. Во многих реализациях производительность async / await лучше, чем у классических callback-функций, при этом такой код читается как синхронный.
Корутины
Описанный выше механизм называется сопрограммой. Часто можно услышать вариант «корутина» (от англ. coroutine — сопрограмма).
Далее будут описаны различные виды и способы организации сопрограмм.
Несколько точек входа
По сути корутинами называются функции, имеющие несколько точек входа и выхода. У обычных функций есть только одна точка входа и несколько точек выхода. Если вернуться к примеру выше, то первой точкой входа будет сам вызов функции оператором asynс , затем функция прервёт своё выполнение вместо ожидания БД или файла. Все последующие await будут не запускать функцию заново, а продолжать её исполнение в точке предыдущего прерывания. Да, во многих языках в корутине может быть несколько await ’ов.
Для большего понимания рассмотрим код на языке Python:
def async_factorial(): result = 1 while True: yield result result *= i fac = async_factorial() for i in range(42): print(next(fac))
Программа выведет всю последовательность чисел факториала с номерами от 0 до 41.
Функция async_factorial() вернёт объект-генератор, который можно передать в функцию next() , а она продолжит выполнение корутины до следующего оператора yield с сохранением состояния всех локальных переменных функции. Функция next() возвращает то, что передаёт оператор yield внутри корутины. Таким образом, функция async_factorial() в теории имеет несколько точек входа и выхода.
Stackful и Stackless
В зависимости от использования стека корутины делятся на stackful, где каждая из корутин имеет свой стек, и stackless, где все локальные переменные функции сохраняются в специальном объекте.
Так как в корутинах мы можем в любом месте поставить оператор yield , нам необходимо где-то сохранять весь контекст функции, который включает в себя фрейм на стеке (локальные переменные) и прочую метаинформацию. Это можно сделать, например, полной подменой стека, как это делается в stackful корутинах.
На рисунке ниже вызов async создаёт новый стек-фрейм и переключает исполнение потока на него. Это практически новый поток, только исполняться он будет асинхронно с основным.
yield в свою очередь возвращает обратно предыдущий стек-фрейм на исполнение, сохраняя ссылку на конец текущего в предыдущий стек.
Наличие собственного стека позволяет делать yield из вложенных вызовов функций, но такие вызовы сопровождаются полным созданием/сменой контекста исполнения программы, что медленней, чем stackless корутины.
Более производительными, но вместе с тем и более ограниченными, являются stackless корутины. Они не используют стек, и компилятор преобразует функцию, содержащую корутины, в конечный автомат без корутин. Например, код:
def fib(): a = 0 b = 1 while True: yield a a += b yield b b += a
Будет преобразован в следующий псевдокод:
class fib: def __init__(self): self.a = 0 self.b = 1 self.__result: int self.__state = 0 def __next__(self): while True: if self.__state == 0: self.a = 0 self.b = 1 if self.__state == 0 or self.__state == 3: self.__result = self.a self.__state = 1 return self.__result if self.__state == 1: self.a += self.b self.__result = self.b self.__state = 2 return self.__result if self.__state == 2: self.b += a self.__state = 3 break
По сути здесь создаётся класс, который сохраняет всё состояние функции, а также последнюю точку вызова yield . У такого подхода есть проблема: yield может быть вызван только в теле функции-корутины, но не из вложенных функций.
Симметричные и асимметричные
Корутины также делятся на симметричные и асимметричные.
Симметричные имеют глобальный планировщик корутин, который и выбирает среди всех ожидающих асинхронных операций ту, которую стоит выполнить следующей. Примером является планировщик, о котором говорилось в начале функции wait_connection() .
В асимметричных корутинах нет глобального планировщика, и программист вместе с поддержкой компилятора сам выбирает, какую корутину и когда исполнять. Большинство реализаций корутин асимметричные.
Вывод
Асинхронное программирование является очень мощным инструментом для оптимизации высоконагруженных программ с частым ожиданием системы. Но, как и любую сложную технологию, её нельзя использовать только потому, что она есть. Необходимо всегда задавать себе вопрос: а нужна ли мне эта технология? Какую практическую пользу она мне даст? Иначе разработчики рискуют потратить очень много сил, времени и денег, не получив никакого профита.
Что такое асинхронное программирование? Принцип работы процессора и многопоточность

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

Работа процессора считается тиками — единица времени, в течение которой выполняется одна задача. Чтобы прочитать данные из двух ячеек, просуммировать их, записать получившуюся сумму в последнюю ячейку, необходимо 4 тика. Все сложные операции программирования (умножение, деление) состоят из простых. Умножение заменяется сложением числа с самим собой несколько раз. Современные процессоры позволяют выполнять одну задачу за несколько миллисекунд. Пользователь не замечает времени выполнения, поэтому ему кажется, что все задачи решаются независимо от других. На самом деле процессор выполняет работу, затем переключается к другому действию. Количество единиц измерения принято считать в герцах (Гц). Данная величина показывает частоту протекания процессов. Современные модели измеряются гигагерцами, включающими триллиарды герцов. Скорость обработки хватает для получения информации, решения множества задач, независимо друг от друга. При чтении программы, процессор использует множество процессов. Затем выдаёт конечное действие на экран пользователя. Если процесс проходит линейно, каждый последующий блок кода попадал в блокировку. При скачивании фильма, компьютером нельзя пользоваться, пока 100% файла не загрузятся на жёсткий диск. Чтобы устройство могло выполнять другие функции во время чтения кода, все действия делятся на потоки. Для создания программ, позволяющих выполнять несколько действий одновременно, рассмотрим термины асинхронность и многопоточность.
Что такое асинхронность в программировании?
Асинхронность — вид программирования, позволяющий вынести выполняемые задачи отдельными блоками кода. Применяется в сервисах, где предыдущее действие тормозит следующее. Синхронный процесс выполняется поэтапно. Пользователь совершает действие, ждёт, пока программа обработает часть кода, переходит к другому блоку. При использовании асинхронности, код убирает операцию, блокирующую следующие действия. Отличить асинхронный код можно визуально. В программировании на C#, имеются 2 слова-индикатора, которые показывают, что задачи работают независимо. К индикаторам относятся функции async и await, упрощающие написание асинхронных блоков. Написание асинхронного кода возможно со всеми параметрами. Ограничений в программировании этого типа нет. Примером асинхронности можно назвать работу с базами данных. Пока пользователю необходима одна информация, остальные блоки отключены. Если человеку нужно совершить новое действие — найти другие данные, код включается. Результат работы кода приходит не сразу после вызова, а при необходимости пользователю просмотреть нужную информацию. Сокращается время обработки процессором.
Что такое многопоточность?
- обновление интерфейса пользователя;
- реагирование на действия пользователя;
- возможность создавать новые потоки;
- запуск дополнительных блоков с кодом.
Чтобы устройство работало быстрее, главный поток должен быть свободным от задач, которые имеют большое время продолжительности. Если в компьютере находится только 1 поток, он должен выполнять все действия: отрисовка интерфейса, скачивание данных из интернета, просмотр фильмов, принятие запросов из интернета. Нагрузив систему, пользователь не может выполнять другие задачи. Главный поток выполняет только одно действие. После завершения переходит к следующему.
Используя многопоточность, можно делить ресурс процессора, выполнять несколько задач одновременно.

Когда необходима асинхронность?
Наиболее распространенная сфера применения асинхронности — использование графическими программами. При работе с ними, основная логика и изменение изображения находятся в разных потоках. Даже когда логика занята, пользователь может совершать действия и выполнять свои задачи.
Представим ситуацию, что программы с графическими интерфейсами состоят из одного потока. При выполнении любой сложной функции, приложение перестает отвечать на запросы пользователя. Такой процесс сильно влияет на работу устройства. При синхронном процессе, алгоритм выполнения задач имеет следующий вид.
- Пользователь отправляет запрос на совершение действия.
- Происходит чтение первой части кода.
- Остальные блоки с кодом блокируются.
- Завершение чтения первой части кода.
- Переход к новому блоку.
При линейном чтении кода уменьшается срок эксплуатации процессоров. Использование синхронного программирования не означает, что любая программа зависнет после запроса. Скорость обработки зависит от ресурса компьютера. Мелкие задачи быстро решаются даже при использовании синхронного вида программирования. Работа с большим количеством данных невозможна при линейном чтении файлов.

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