asyncio: почему не неблокируется по умолчанию - PullRequest
0 голосов
/ 12 ноября 2018

По умолчанию asyncio выполняет сопрограммы синхронно. Если они содержат блокирующий код ввода-вывода, они все еще ждут его возврата. Обходной путь это loop.run_in_executor(), который преобразует код в потоки. Если поток блокируется на IO, другой поток может начать выполняться. Таким образом, вы не тратите время на ожидание вызовов IO.

Если вы используете asyncio без исполнителей, вы теряете эти ускорения. Поэтому мне было интересно, почему вы должны использовать исполнителей явно. Почему бы не включить их по умолчанию? (Далее я сосредоточусь на запросах http. Но они действительно служат только примером. Меня интересуют общие принципы.)

После некоторых поисков я нашел aiohttp . Это библиотека, которая по сути предлагает комбинацию asyncio и requests: неблокирующие HTTP-вызовы. С исполнителями asyncio и requests ведут себя почти так же, как aiohttp. Есть ли причина для внедрения новой библиотеки, платите ли вы штраф за производительность за использование исполнителей?

На этот вопрос ответили: Почему Asyncio не всегда использует исполнителей? Михаил Герасимов объяснил мне, что исполнители раскрутят OS-нити и могут стать дорогими. Поэтому имеет смысл не использовать их в качестве поведения по умолчанию. aiohttp лучше, чем использовать модуль requests в исполнителе, поскольку он предлагает неблокирующий код только с сопрограммами.

Что подводит меня к этому вопросу. aiohttp рекламирует себя как:

Асинхронный HTTP клиент / сервер для asyncio и Python.

Итак, aiohttp основан на asyncio? Почему тогда asyncio не предлагает неблокирующий код только с сопрограммами? Это было бы идеальным значением по умолчанию.

Или aiohttp сам реализовал этот новый цикл обработки событий (без потоков ОС)? В этом случае я не понимаю, почему они рекламируют себя как основанные на asyncio. Async/await является языковой функцией. Asyncio это цикл обработки событий. И если aiohttp имеет свой собственный цикл обработки событий, то пересечение с asyncio должно быть незначительным. На самом деле, я бы сказал, что такой цикл обработки событий был бы гораздо большей функцией, чем запросы http.

Ответы [ 2 ]

0 голосов
/ 12 ноября 2018

Итак, aiohttp основан на asyncio?

Да, он основан на абстракциях asyncio, таких как фьючерсы , транспорты и протоколы , примитивы синхронизации и т. Д.

Почему тогда asyncio не предлагает неблокирующий код только с сопрограммами?

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

aiohttp использует все эти функции для реализации работоспособного http-клиента и сервера поверх asyncio.

Или aiohttp внедряютсам этот новый цикл обработки событий (без потоков ОС)?

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

Async / await - это языковая функция,Asyncio - это цикл обработки событий.

Async / await - это языковая функция, такая как генераторы.Asyncio - это библиотека, которая использует их, как itertools.Есть и другие библиотеки, которые используют сопрограммы, например, curio и trio .

0 голосов
/ 12 ноября 2018

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

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

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

Но большая часть этого кода также не поточнобезопасна, обычно это не требуется. Но как только asyncio выполнит весь такой код в исполнителе автоматически , вы не сможете больше использовать функции, не поддерживающие потоки. Кроме того, создание потока для запуска синхронного кода не является бесплатным, создание объекта потока стоит времени, и ваша ОС также не позволит вам запускать бесконечное количество потоков. Множество стандартных библиотечных функций и методов: fast , зачем вам запускать str.splitlines() или urllib.parse.quote() в отдельном потоке, когда гораздо быстрее просто выполнить код и покончить с этим?

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

Ответ на этот вопрос таков: вы не можете. time.sleep() - это блокирующая функция, для которой вы хотите уступить циклу, но это вызов функции C. Python не может знать , что time.sleep() будет блокировать дольше, потому что функция, вызывающая time.sleep(), будет искать имя time в глобальном пространстве имен, а затем атрибут sleep на результат поиска имени, только когда фактически выполняется выражение time.sleep(). Поскольку пространства имен Python могут быть изменены в любой момент во время выполнения , вы не можете знать, что будет делать time.sleep(), пока вы на самом деле не выполните функцию.

Можно сказать, что реализация time.sleep() должна автоматически выдавать при вызове тогда, но тогда вам придется начинать определять все такие функции. И нет никаких ограничений на количество мест, которые вы должны будете патчить, и вы никогда не сможете узнать все места. Конечно, не для сторонних библиотек. Например, проект python-adb обеспечивает синхронное USB-подключение к устройству Android с использованием библиотеки libusb1. Это не стандартный путь ввода-вывода, поэтому как Python узнает, что создание и использование этих соединений - хорошие места для получения?

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

Так как же сопрограммы под asyncio взаимодействуют? Используя task objects на логический фрагмент кода, который должен выполняться одновременно с другими задачами, и используя future objects , чтобы подать сигнал задача, которую текущий логический фрагмент кода хочет передать управление другим задачам. Вот что делает асинхронный код asyncio асинхронным, добровольно передавая управление. Когда цикл дает управление одной задаче из многих, задача выполняет один «шаг» цепочки вызовов сопрограммы, пока эта цепочка вызовов не создаст объект будущего, после чего задача добавляет обратный вызов wakeup к будущему объекту «готово» список обратных вызовов и возвращает управление в цикл. В какой-то момент позже, когда будущее помечено как выполненное, выполняется обратный вызов пробуждения, и задача выполнит еще один шаг цепочки вызовов сопрограммы.

Что-то иначе отвечает за маркировку будущих объектов как выполненных. Когда вы используете asyncio.sleep(), обратный вызов, который будет запущен в определенное время, передается в цикл, где этот обратный вызов помечает будущее asyncio.sleep() как выполненное. Когда вы используете потоковый объект для выполнения операций ввода-вывода, затем (в UNIX) цикл использует select вызовов , чтобы определить, когда пришло время пробуждать будущий объект, когда операция ввода / вывода завершена. А когда вы используете блокировку или другой примитив синхронизации , то примитив синхронизации будет поддерживать кучу фьючерсов, чтобы пометить их как «выполненные», когда это необходимо (Ожидание блокировки? Добавить будущее в кучу. удерживать блокировку? Выбрать следующее будущее из кучи и отметить его как выполненное, чтобы следующее задание, которое ожидало блокировку, могло проснуться и получить блокировку и т. д.).

Помещение синхронного кода, который блокирует исполнителя, - это просто еще одна форма сотрудничества. При использовании asyncio в проекте, developer должен убедиться, что вы используете предоставленные вам инструменты для обеспечения взаимодействия ваших сопрограмм. Вы можете использовать блокировку вызовов open() для файлов вместо потоков, и вы можете использовать исполнителя, если вы знаете, что код должен выполняться в отдельном потоке, чтобы избежать слишком длительной блокировки.

И последнее, но не менее важное: весь смысл использования asyncio состоит в том, чтобы избегать максимально возможного использования многопоточности. Использование потоков имеет свои недостатки; код должен быть поточно-безопасным (элемент управления может переключаться между потоками в любом месте , поэтому два потока, обращающиеся к общему фрагменту данных, должны делать это с осторожностью, а «забота» может означать, что код замедлен ). Потоки выполняются независимо от того, есть у них что-то делать или нет; переключение управления между фиксированным числом потоков, которые all ждут, пока произойдет ввод / вывод, - это пустая трата процессорного времени, когда цикл asyncio свободен для поиска задачи, которая не ожидает.

...