Глава 1. Введение

Моя история очень похожа на вашу, только она более интересна, потому что в ней роботы.

-- Бендер, Футурама, эпизод "Повар, на 30 % из железа"

Наиболее распространённым вопросом, который я получаю относительно Asyncio в Python 3 такой: "А что это такое и что мне с ним делать?" Приводимая далее история даёт вам обстановку для ответа на эти вопросы. Основным средоточием Asyncio является то, как наилучшим образом выполнять множество параллельных задач одновременно. Причём не просто задачи любого вида, а в особенности те, которые содержат временные периоды ожидания. Основная ключевая мысль, необходимая для такого стиля программирования состоит в том, что пока вы ожидаете пока эта задача завершится, может выполняться работа с прочими задачами.

Ресторан Серийных ботов

Сейчас 2051 год, а вы обнаруживаете себя в ресторанном деле! Автоматизация, по большей части роботами- рабочими, но, как оказывается, люди всё ещё время- от - времени любят поесть. В вашем ресторане все сотрудники являются роботами: естественно, гуманоиды, но вне всяких сомнений роботы. Наиболее успешным производителем роботов, конечно же, является Threading Inc., а роботы- исполнители от этой компании стали именоваться "ThreadBots" {Поточные боты - Серийные боты}.

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

Естественно, являясь новичком в роботизированном ресторанном бизнесе, вы делаете то же, что и прочие рестораторы и вы нанимаете небольшую флотилию роботов: одного для приёма новых посетителей на стойке регистрации (hostbot, метрдотель); одного ожидающего у столика для приёма заказов (waitbot, официант); одного на готовку (chefbot, повар); и одного на управление баром (winebot, бармен).

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

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

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

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

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

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

Другая проблема ресурсов, помимо дополнительной стоимости, естественно, состоит в том, что стало гораздо больше работы для вас чтобы присматривать за этими дополнительными Серийными ботами. Было здорово следить за четырьмя ботами, но теперь их стало семь! Держать на глазу семь Серийных ботов это намного больше работы, а поскольку ваш ресторан становится всё более и более популярным, вы начинаете беспокоиться о том, чтобы привлечь к работе ещё больше Серийных ботов. Это станет самостоятельным занятием с полной занятостью отслеживать то, что делает каждый Серийный бот! И опять же: эти дополнительные Серийные боты потребляют намного больше пространства внутри вашего ресторана. Ваши посетители оказываются достаточно сильно стеснёнными со всеми этими снующими туда и обратно роботами. Вы обеспокоены тем, что когда вам потребуется добавить ещё больше ботов, данная проблема пространства станет ещё более сильной. Вы желаете использовать пространство ресторана для посетителей, а не для Серийных ботов!

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

Время идёт.

Затем, во время одного очень- очень загруженного вечера в пятницу у вас наступает особый момент прояснения: время замедляется, прояснение снисходит на вас и вы видите некий моментальный снимок вашего замороженного на мгновение ресторана. Мои Серийные боты ничего не делают! Честно говоря, не совсем ничего, но они просто ... ждут.

Каждый из ваших трёх ботов официантов на различных столиках ожидает пока один из посетителей за их столом сделает им заказ. Бот бармен уже приготовил 17 напитков, которые теперь ожидают пока их заберут (это отнимет всего несколько секунд), и теперь он ждёт новых заказов на напитки. Один их ботов метрдотелей встречает новую группу гостеу, говорит что им следует подождать минутку, пока он их рассадит и ждёт ответа от этих гостей. Другой бот метрдотель теперь обрабатывает некий платёж кредитной картой для следующего посетителя, который собирается покинуть заведение и ожидает подтверждения данного платежа от терминального устройства. Даже ваш бот повар, который в настоящий момент готовит 35 блюд, на самом деле не делает ничего в данный момент, а просто ожидает пока одно из этих блюд завершит готовиться с тем, чтобы поместить его на поднос, который в руках у одного из ботов официантов.

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

Момент истины проходит, но его осознание остаётся. Вы ждёте окончания недели и первое что вы делаете, вы добавляете модуль сбора данных для ваших Серийных ботов. Для каждого из Серийных ботов вы измеряете сколько времени потрачено на выполнение его работы. На протяжении следующей недели собираются все данные и в воскресенье вечером вы анализируете полученные результаты. Оказывается, даже когда ваш ресторан забит под завязку, ваш самый загруженный Серийный бот простаивает около 98% всего времени! Ваши Серийные боты настолько экономически эффективны, что они способны выполнять любую задачу за доли секунды.

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

Итак, на следующий же день, который является спокойным понедельником, вы пытаетесь сделать нечто очень смелое: вы программируете одного из своих Серийных ботов на выполнение всех имеющихся задач. И всякий раз когда он начинает ждать, даже хотя бы секунду, вместо ожидания, этот Серийный бот будет переключён на следующую задачу, которая может возникнуть во всём ресторане. Это звучит невероятно на первый взгляд, только один Серийный бот выполняет всю работу всех оставшихся, но вы уверены, что ваши вычисления верны. И к тому же, понедельник - очень спокойный день, поэтому если что- то пойдёт не так, потери будут небольшими. Для такого нового проекта вы именуете своего бота "loopbot" (циклическим ботом, фигаро), потому что он в цикле выполняет все задания в вашем ресторане.

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

Наступил понедельник, и вы видите, что ваш бот фигаро как ястреб. Он перемещается между позициями за доли секунды, проверяя нет ли для него какой работы. Через какое- то время после открытия за стойкой появляется самый первый посетитель. Ваш бот фигаро появляется почти мгновенно и спрашивает не желает ли клиент присесть за столик у окна или у стойки бара. А затем, поскольку этот бот фигаро начинает ждать, его программа заставляет его переключиться на следующую задачу и он со свистом уносится прочь! Это выглядит как ужасная ошибка, но вы видите, что ваш посетитель начинает говорить "у окна, пожалуйста", и бот фигаро снова на месте! Он принимает ответ и проводит гостя к столику 42. И покидает гостя снова, проверяя наличие заказа на напитки, еду, уборку стола и приём гостей снова и снова.

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

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

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

К сожалений, то- то идёт не так: подгорело одно из блюд, сложное суфле! Этого никогда раньше не случалось в вашем ресторане. Вы начинаете изучать своего циклического бота более пристально. Оказывается, за одним из ваших столиков находится очень болтливый посетитель. Этот гость пришёл в ваш ресторан один и продолжает попытки поддержать разговор с вашим ботом фигаро, даже когда ваш бот фигаро что- то держит в руках. Когда это происходит, ваш бот фигаро не может отойти и отслеживать весь всё растущий перечень задач в прочих местах вашего ресторана. Именно так вам повар и допустил своё первое в жизни подгоревшее суфле. Ваш бот фигаро не имел возможности вернуться назад на кухню чтобы снять суфле с огня, так как он был занят каким- то посетителем.

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

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

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

Прошло ещё какое- то время.

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

Эпилог

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

В ресторане время его исполнителя, затрачиваемое на ожидание, не является гигантским когда медленный людской персонал делает свою работу вручную, однако когда сверх- эффективные и быстрые роботы берутся за эту работу, тогда почти всё их время тратится на ожидание. При программировании компьютера то же самое справедливо при вовлечении сетевого программирования. ЦПУ выполняют свою "работу" и "ждут" сетевого ввода/ вывода. В современных компьютерах ЦПУ чрезвычайно быстрые, в сотни и в тысячи раз быстрее чем сетевой обмен. Таким образом, исполняющие сетевые программы ЦПУ тратят слишком много времени на ожидание. {Прим. пер.: пожалуй, слишком ограничивающее утверждение, при наличии интерконнекта с очень высокой полосой пропускания и низкими значениями латентности тщательный подбор параметров обмена становится ещё более актуальным, например, см. Эффективное применение RDMA для служб ключ- значение, Рекомендации по разработке высокопроизводительных систем RDMA.}

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

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

Какие проблемы пытается решать Asyncio?

Для связанных со вводам/ выводом рабочих нагрузок существует в точности две причины (только!) для использования основанной на асинхронности одновременности перед одновременностью на основе потоков:

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

  • Asyncio предлагает простой способ поддержки многих тысяч одновременных взаимодействий сокетов, в том числе способных обрабатывать долго живущие соединения для новейших технологий, таких как websockets или MQTT в приложениях интернета- вещей (IoT).

Это всё.

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

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

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

  • Asyncio сделает мой код явно быстрее

    К сожалению, нет. На самом деле, большинство эталонных тестов демонстрируют что решения с потоками являются слегка более быстрыми при сопоставлении с решениями Asyncio. Если значение дополнительной параллельности само по себе рассматривается в качестве измерения производительности, Asyncio, тем не менее, на самом деле делает его слегка быстрее за счёт создания очень большого числа одновременных соединений сокетов. Операционные системы зачастую имеют ограничения относительно того, сколько потоков можно создавать и это число значительно ниже чем общеее число соединений сокетов, которое она может выполнять. Эти пределы ОС могут быть изменены, однако несомненно проще это делать при помощи Asyncio. И пока мы ожидаем, что наличие многих тысяч потоков должно принять на себя дополнительную стоимость переключения контекста, которого избегают сопрограммы (coroutines), на практике это сложно подвергнуть эталонному тестированию [Представляется, что исследования в этой области сложно отыскать, но их значения, как кажется, находятся примерно в пределах 50 микросекунд на переключение контекста потока в Linux на современном оборудовании. Чтобы представить (очень) примерную оценку: тысяча потоков подразумевает суммарную стоимость в 50 мс для переключения контекстов. Оно действительно добавляется, но ни коим образом не будет разрушать ваше приложение.] Нет, скорость не является основным преимуществом Asyncio в Python; если это то что вам требуется, попробуйте вместо этого Cython!

  • Asyncio делает деление на потоки более устойчивым

    Определённо нет! Истинное назначение потоков состоит в наличии способности создавать программы со множеством ЦПУ, в которых различные вычислительные задачи могут совместно использовать память. Имеющиеся численные библиотеки, numpy, к примеру, уже используют это расщепляя определённые матричные вычисления на использование множества ЦПУ, даже хотя вся память используется совместно для чистой производительности не существует конкурентов такой модели программирования при вычислениях, ограниченных ЦПУ.

  • Asyncio удаляет все проблемы с имеющимся GIL

    Опять нет. Это правда,что Asyncio не испытывает воздействия со стороны GIL [GIL (global interpreter lock, глобальная блокировка интерпретатора) делает код интерпретатора Python (а не ваш код!) потокобезопасным, блокируя обработку каждого кода операции; у этого имеется неудачный сторонний эффект действенного закрепления данного исполнения этого интерпретатора на отдельном ЦПУ и, таким образом, предотвращая параллельность множества ядер.], однако происходит только потому, что сам GIL оказывает воздействие на многопоточные программы. Основные "проблемы" с GIL, на которые ссылаются люди состоят в том, что он предотвращает истинный многоядерный подход распараллеливания при применении потоков. Так как Asyncio является однопоточным (почти по определению), он не подвержен воздействию GIL, однако он также не может получать преимуществ от множества ядер ЦПУ. [Это аналогично тому как JavaScript не имеет "проблемы" GIL: имеется всего один поток.] Стоит также отметить, что при многопоточном кодировании, имеющийся GIL Python может вызывать дополнительные проблемы производительности, выходящие за рамки тех, о которых уже упоминалось в прочих пунктах: Дейв Бизли выступил с докладом "Осознание Python GIL" на PyCon 2010, и многое из того, что обсуждалось там, остаётся верным и по сей день.

  • Asyncio предотвращает все условия состязательности

    Ложь. Возможность условий состязательности всегда присутствует в любом параллельном программировании, вне зависимости от того, кокое программирование применяется: с потоками или на основе событий. Это правда, что Asyncio может фактически предотвращать определённый класс условий состязательности, общих для программ со множеством потоков, таких как межпроцессный доступ к общей памяти. Однако он не исключает возможность прочих видов условия конкуренции, к примеру, межпроцессные состязания относительно совместно используемых ресурсов, распространённых в архитектурах с распределёнными микрослужбами. Вам всё ещё следует уделять внимание тому как используются совместные ресурсы. Основное преимущество Asyncio относительно кодирования потоков состоит в том, что те пункты, в которых контролируется исполнение передаются между сопрограммами являются видимыми (благодаря наличию ключевых слов await) и гораздо проще обсуждать о доступе к совместным ресурсам.

  • Asyncio делает параллельное программирование намного более простым

    Хм, давайте даже не начинать?

Самый последний миф - самый опасный. Работа с параллельностью всегда сложна, вне зависимости от того применяете ли вы потоки или Asyncio. Когда эксперт говорит "Asyncio делает распараллеливание более простым", то что он на самом деле имеет в виду, так это то, что он делает более простым обход определённых видов реальных ночных кошмаров ошибок условий состязательности; которые оставляют вас на ночь и о которых вы рассказываете прочим программистам в приглушённых тонах вокруг кострища волком, воющим на расстоянии.

Даже при Asyncio остаётся значительное место сложности, с которой приходится иметь дело. Как ваше приложение осуществляет поддержку проверки состояния здоровья? Как вы можете осуществлять взаимодействие с базой данных, которая может допускать только несколько подключений, намного меньше чем ваши 5 000 соединений с клиентами? Как вы пожелаете чтобы ваша программа аккуратно прекратила подключения после получения некого сигнала на останов? Как вы обработаете (блокирующий!) доступ к диску и протоколирование? Это всего несколько из множества сложных проектных решений, на которые вам придётся отвечать.

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