Что такое асинхронность в программировании
Перейти к содержимому

Что такое асинхронность в программировании

  • автор:

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

Акчурин И. С.

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

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

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

Где асинхронность применяется в реальном мире

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

Основы Python
Курс для IT-специалистов

Например, вы провожаете родственников и просите их позвонить, как только они доберутся до дома. А дальше спокойно занимаетесь своими делами и не названиваете им каждую минуту с проверкой: «Уже доехали?» Потому что звонок от родственников поступит сам, как только они доберутся до дома.

То есть функция «Звонок от родственников» ожидает события «Родственники приехали домой». И как только это событие произойдет, функция сразу сработает.

Или вы ставите напоминание в календаре на конкретную дату и время: «Созвон с коллегами». А потом напрочь забываете об этом событии. Но календарь не забудет!

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

Особенности асинхронного программирования на Python

С чего начать изучение Python, как поймать Python-дзен и для чего этот язык пригодится в повседневной жизни, рассказываем на бесплатном вебинаре «Зачем специалисту Python?».

Обычные функции

Давайте запрограммируем такую ситуацию с уведомлением. Повседневную жизнь представим с помощью функции main ( ) . А уведомление — с помощью функции notification ( ) .

Внутри функции notification ( ) находится задержка в 10 секунд, чтобы имитировать отложенное задание. Если запустить код, то мы увидим, что поговорить с коллегой и поесть не удастся, потому что программа застынет на выполнении notification ( ) и будет ждать 10 секунд, пока уведомление не придет и не спасет от голодной смерти ��

Очевидно, что есть и разговаривать с коллегой мы хотим в то время, пока идет обратный отсчет до уведомления. То есть важно сделать так, чтобы работа функции notification ( ) не прерывала работу функции main ( ) .

Корутины

Чтобы решить этот вопрос, необходимо функцию notification ( ) сделать асинхронной, то есть работающей независимо от всего остального. Для этого в начале приписываем магическое async.

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

Что же произошло? async — это оператор Python, обертка над основной функцией. Теперь, когда вы вызовете функцию notification ( ) , она вернет специальный объект — корутину (coroutine).

  • Корутина тоже представляет собой функцию, которую нужно вызвать отдельно. Об этом и сообщает Python.

Обычный вызов notification ( ) теперь не исполняет функцию, а превращает ее в корутину, которую нужно вызвать. Чтобы это сделать, используем оператор await .

Теперь запуск кода приведет к новой проблеме:

Python будет выполнять await только в том случае, если он будет вызван внутри асинхронной функции. То есть функцию main ( ) необходимо тоже сделать асинхронной.

Но тогда мы получили еще одну корутину, которую также кто-то должен вызвать и сделать это опять только внутри асинхронной функции! Что же делать? Замкнутый круг ��

Asyncio спешит на помощь

Для управления асинхронными функциями в Python используется специальный модуль asyncio. Его поддержка впервые появилась в версии Python 3.5 в 2015 году. Добавим его к программе и запустим корутину main ( ) , используя метод run( ).

В терминах асинхронного программирования функция main ( ) будет называться циклом обработки событий (event loop). Это основной цикл программы, который вызывает все асинхронные функции, необходимые для работы.

Теперь программа вновь заработает! Правда, уведомление о созвоне снова не даст поесть и пообщаться с коллегой… Все опять работает как обычная последовательность действий.

Дело в том, что недостаточно просто запустить корутину — необходимо создать задачу (task), которая это сделает.

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

Запустите программу и обратите внимание на последовательность работы функции print( ). Несмотря на то что «Едим» и «Разговариваем с коллегой» выполняются после вызова корутины notification ( ) , мы увидим их первыми, потому что корутина выполняется асинхронно относительно основного цикла обработки событий main ( ) .

Знатоки Python могут закидать меня помидорами за такой пример, так как на самом деле time.sleep( ) нельзя использовать при создании нескольких асинхронных событий внутри event loop. А также не хватает вызова корутины через await . Но не торопитесь: обо всем по порядку.

Когда работы много

Зачем специалисту Python?
Бесплатный вебинар

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

Создадим основную функцию, внутри которой будем вызывать функции, возвращающие данные для погодной станции. Чтобы имитировать задержки, воспользуемся функцией time.sleep( ). Также будем измерять общее время
работы программы с помощью time.time( ).

Традиционное решение — вызов функций друг за другом. Так как выполнение main ( ) приостанавливается в момент вызова get_temp ( ) и get_pres ( ) , несложно подсчитать, что общее время работы будет равно сумме времени, затраченного на выполнение всех функций, вызываемых внутри main ( ) .

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

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

Почему так происходит? Все дело в задержках, которые мы создали. Асинхронно сейчас выполняется только main ( ) (цикл событий) относительно функций, которые он вызывает. Это видно по моментальному выводу о начале и конце измерения еще до того, как мы получили непосредственно сами данные.

А вот функции get_temp ( ) и get_pres ( ) выполняются последовательно, потому что внутри содержат обычную функцию time.sleep( ), тормозящую всю работу. Чтобы исправить этот эффект, необходимы корутины, которые будут создавать задержку и при этом не будут тормозить вызов других функций. Исправьте time.sleep( ) на asyncio.sleep( ) и не забудьте про await , ведь теперь мы работаем с корутинами.

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

Опять что-то не так… На самом деле все теперь работает корректно. Просто цикл событий main ( ) не дожидается окончания работы асинхронных функций и завершает свою работу раньше, чем должен. У нас же теперь все асинхронное, зачем ему кого-то ждать? ��

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

И время выполнения программы сократилось до 4 секунд вместо 6:

Теперь в цикл событий main ( ) можно добавить сколько угодно задач через asyncio. А общее время его работы всегда будет определяться временем работы самой долгой функции. Добро пожаловать в асинхронный мир!

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

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

Обложка поста Асинхронность в программировании

Традиционно в программировании используют синхронное программирование — последовательное выполнение инструкций с синхронными системными вызовами, которые полностью блокируют поток выполнения, пока системная операция, например чтение с диска, не завершится. В качестве примера ниже написан 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 в свою очередь возвращает обратно предыдущий стек-фрейм на исполнение, сохраняя ссылку на конец текущего в предыдущий стек.

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

Наличие собственного стека позволяет делать 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() .

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

Вывод

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

Что такое асинхронность в программировании? [дубликат]

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

Отслеживать
13k 10 10 золотых знаков 41 41 серебряный знак 78 78 бронзовых знаков
задан 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. Это частный случай многопоточного программирования.

Для чего нужна асинхронность?

Нам всем нравится, как Rust позволяет нам писать быстрое и безопасное программное обеспечение. Но как асинхронное программирование вписывается в это видение?

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

Асинхронность и другие модели параллелизма

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

  • Потоки ОС не требуют каких-либо изменений в модели программирования, что упрощает реализацию параллелизма. Однако синхронизация между потоками может быть затруднена, а издержки производительности велики. Пулы потоков могут снизить некоторые из этих затрат, но не настолько, чтобы поддерживать огромные рабочие нагрузки, связанные с вводом-выводом.
  • Программирование, управляемое событиями (event-driven programming), в сочетании с обратными вызовами (callbacks) может быть очень эффективным, но приводит к многословному, «нелинейному» потоку управления. Поток данных и распространение ошибок часто трудно отслеживать.
  • Корутины (Coroutines), как и потоки, не требуют изменений в модели программирования, что делает их простыми в использовании. Как и асинхронность, они также могут поддерживать большое количество задач. Однако они абстрагируются от низкоуровневых деталей, важных для системного программирования и разработчиков пользовательских сред выполнения.
  • Модель акторов делит все параллельные вычисления на единицы, называемые акторами, которые взаимодействуют посредством передачи ошибочных сообщений, как в распределённых системах. Модель акторов может быть эффективно реализована, но она оставляет без ответа многие практические вопросы, такие как управление потоком и логика повторных попыток.

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

Асинхронность в Rust против других языков

Хотя асинхронное программирование поддерживается на многих языках, некоторые детали зависят от реализации. Реализация асинхронности в Rust отличается от большинства языков несколькими способами:

  • Футуры инертны в Rust и работают только при опросе. Сбрасывание футуры останавливает её дальнейший прогресс.
  • Асинхронность в Rust бесплатна (zero-cost), а это значит, что вы платите только за то, что используете. В частности, вы можете использовать асинхронность без распределения кучи и динамической диспетчеризации, что отлично подходит для производительности! Это также позволяет использовать асинхронность в средах с ограничениями, таких как встроенные системы.
  • В Rust нет встроенной среды выполнения асинхронности. Вместо этого такие среды предоставляются трейтами, поддерживаемыми сообществом.
  • В Rust доступны как однопоточные, так и многопоточные среды выполнения, которые имеют разные сильные и слабые стороны.

Асинхронность против потоков в Rust

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

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

Асинхронность значительно снижает нагрузку на ЦП и память, особенно для рабочих нагрузок с большим количеством задач, связанных с вводом-выводом, таких как серверы и базы данных. При прочих равных у вас может быть на порядки больше задач, чем потоков ОС, потому что асинхронная среда выполнения использует небольшое количество (дорогих) потоков для обработки большого количества (дешёвых) задач. Однако асинхронный Rust приводит к большим двоичным объектам из-за конечных автоматов, сгенерированных из асинхронных функций, и поскольку каждый исполняемый файл включает в себя асинхронную среду выполнения.

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

Пример: одновременная загрузка

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

fn get_two_sites() < // Spawn two threads to do work. let thread_one = thread::spawn(|| download("https://www.foo.com")); let thread_two = thread::spawn(|| download("https://www.bar.com")); // Wait for both threads to complete. thread_one.join().expect("thread one panicked"); thread_two.join().expect("thread two panicked"); >

Однако загрузка веб-страницы — несложная задача; создание потока для такого небольшого объёма работы довольно расточительно. Для более крупного приложения это может легко стать узким местом. В асинхронном Rust мы можем выполнять эти задачи одновременно без дополнительных потоков:

async fn get_two_sites_async() < // Create two different "futures" which, when run to completion, // will asynchronously download the webpages. let future_one = download_async("https://www.foo.com"); let future_two = download_async("https://www.bar.com"); // Run both futures to completion at the same time. join!(future_one, future_two); >

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

Пользовательские модели параллелизма в Rust

И наконец, Rust не заставляет вас выбирать между потоками и асинхронностью. Вы можете использовать обе модели в одном и том же приложении, что может быть полезно, когда у вас есть смешанные многопоточные и асинхронные зависимости. На самом деле вы даже можете использовать другую модель параллелизма, например программирование, управляемое событиями (event-driven programming), если найдёте библиотеку, которая её реализует.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *