asyncio: почему не неблокируется по умолчанию
По умолчанию, 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
реализовать этот новый цикл событий (без OS-потоков) сам? В этом случае я не понимаю, почему они рекламируют себя как основанные на asyncio
, Async/await
являются языковой особенностью. Asyncio
это цикл обработки событий И если aiohttp
имеет свой собственный цикл обработки событий, там должно быть небольшое пересечение с asyncio
, На самом деле, я бы сказал, что такой цикл обработки событий будет гораздо большей функцией, чем HTTP-запросы.
2 ответа
asyncio
является асинхронным, потому что сопрограммы сотрудничают добровольно. Все asyncio
код должен быть написан с учетом сотрудничества, в этом все дело. В противном случае вы можете использовать потоки исключительно для достижения параллелизма.
Вы не можете запускать "блокирующие" функции (не сопрограммированные функции или методы, которые не будут взаимодействовать) в исполнителе, потому что вы не можете просто предположить, что этот код может быть запущен в отдельном потоке исполнителя. Или даже если это нужно запустить в исполнителе.
Стандартная библиотека Python полна действительно полезного кода, который asyncio
проекты захотят использовать. Большая часть стандартной библиотеки состоит из регулярных, блокирующих функций и определений классов. Они выполняют свою работу быстро, поэтому, несмотря на то, что они "блокируют", они возвращаются в разумные сроки.
Но большая часть этого кода также не поточнобезопасна, обычно это не требуется. Но как только asyncio
будет запускать весь такой код в исполнителе автоматически, тогда вы не сможете больше использовать не поточнобезопасные функции. Кроме того, создание потока для запуска синхронного кода не является бесплатным, создание объекта потока стоит времени, и ваша ОС также не позволит вам запускать бесконечное количество потоков. Загрузка стандартных библиотечных функций и методов происходит быстро, зачем вам запускать 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
сотрудничать? Используя объектызадач для каждого логического фрагмента кода, который должен выполняться одновременно с другими задачами, и используя будущие объекты, чтобы сигнализировать задаче, что текущий логический фрагмент кода хочет передать управление другим задачам. Вот что делает асинхронным asyncio
Код асинхронный, добровольно сдающий управление. Когда цикл дает управление одной задаче из многих, задача выполняет один "шаг" цепочки вызовов сопрограмм, пока эта цепочка вызовов не создаст будущий объект, после чего задача добавляет обратный вызов пробуждения к будущему объекту "done". 'список обратных вызовов и возвращает управление в цикл. В какой-то момент позже, когда будущее помечено как выполненное, выполняется обратный вызов пробуждения, и задача выполнит еще один шаг цепочки вызовов сопрограммы.
Что - то еще отвечает за маркировку будущих объектов как выполненных. Когда вы используете asyncio.sleep()
обратный вызов для запуска в определенное время передается в цикл, где этот обратный вызов будет отмечатьasyncio.sleep()
будущее как сделано. Когда вы используете потоковый объект для выполнения ввода / вывода, тогда (в UNIX) цикл используетselect
вызывает, чтобы определить, когда пришло время пробуждать будущий объект, когда операция ввода / вывода завершена. А когда вы используете блокировку или другой примитив синхронизации, то примитив синхронизации будет поддерживать кучу фьючерсов, чтобы пометить их как "выполнено", когда это необходимо (Ожидание блокировки? Добавить будущее в кучу. Освобождение удерживаемой блокировки? Выбрать следующую будущее из кучи и пометьте его как выполненное, чтобы следующее задание, которое ожидало блокировки, могло проснуться и получить замок и т. д.).
Внедрение синхронного кода, который блокирует исполнителя, - это просто еще одна форма сотрудничества. Когда используешь asyncio
В проекте разработчик должен убедиться, что вы используете предоставленные вам инструменты, чтобы обеспечить совместимость ваших сопрограмм. Вы можете использовать блокировку open()
вызывает файлы вместо использования потоков, и вы можете использовать исполнителя, если вы знаете, что код должен выполняться в отдельном потоке, чтобы избежать слишком длительной блокировки.
Не в последнюю очередь, весь смысл использования asyncio
это избегать использования многопоточности как можно больше. Использование потоков имеет свои недостатки; код должен быть потокобезопасным (управление может переключаться между потоками в любом месте, поэтому два потока, обращающиеся к общему фрагменту данных, должны делать это с осторожностью, а "забота" может означать, что код замедляется). Потоки выполняются независимо от того, есть у них что-то делать или нет; переключение управления между фиксированным числом потоков, которые все ожидают ввода-вывода, является пустой тратой процессорного времени, где asyncio
Цикл свободен, чтобы найти задачу, которая не ждет.
Так
aiohttp
основывается наasyncio
?
Да, он основан на абстракциях asyncio, таких как фьючерсы, транспорты и протоколы, примитивы синхронизации и так далее.
Почему не
asyncio
предложить неблокирующий код только с сопрограммами?
Если вы используете asyncio API, это именно то, что он делает. Он предлагает неблокирующий код для подключения к серверу, разрешения имени хоста, создания сервера и даже запуска кода блокировки в отдельном пуле потоков без блокировки цикла событий.
aiohttp использует все эти функции для реализации работоспособного http-клиента и сервера поверх asyncio.
Или сделал
aiohttp
реализовать этот новый цикл событий (без OS-потоков) сам?
Нет, aiohttp подключается к циклу событий asyncio. Точнее, приложение, которое использует aiohttp, раскручивает цикл событий asyncio и подключает к нему aiohttp (и другие библиотеки на основе asyncio).
Async / await - это языковая функция. Asyncio - это цикл событий.
Async / await - это языковая функция, такая как генераторы. Asyncio - это библиотека, которая использует их, как itertools. Есть и другие библиотеки, в которых используются сопрограммы, например, curio и trio.