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
свободен для поиска задачи, которая не ожидает.