Comments 25
Использование
ThreadPoolExecutor
для запуска синхронных функций в асинхронном коде открывает новые горизонты для разработки высокопроизводительных приложений.
Нет. Использование CPU bound задач через асинхронные функции на пуле потоков, будет равноценно использованию этих же функций синхронно на пуле потоков. Асинхронность не принесёт никаких плюсов, только сложность.
Стратегия использовать несколько пулов потоков для разных задач нормальная. Так например сделанны десктопные приложения: один поток рисует, другие обрабатывают кнопочки.
@routes.get('/random')
async def handle_batch(request: Request) -> Response:
with ThreadPoolExecutor() as executor:
tasks = [run_in_executor_main(executor) for _ in range(3)]
results = await asyncio.gather(*tasks)
Как только количество запросов дорастёт до executor.max_workers всё встанет.
А возможно еще раньше, так как вы используете количество тредов по умолчанию и можете получить больше тредов чем ядер на машине `min(32, os.cpu_count() + 4)`. Тогда GIL начнет влиять и на главный поток.
По-моему, сервер может спокойно обрабатывать запросов больше, чем min(32, os.cpu_count() + 4)
Как перед написанием статьи разобраться с тем, что такое асинхронность и многопоточность.
Статью копипастил маркетолог. Не царское это дело - разбираться в предмете статьи.
шах и мат, сдаюсь :)
Шутки шутками, но это реально проблема.
Недавно на очередном собесе вопрос: "Чем отличаются multiprocess, multithreading и async?"
А я как собака - знать знаю, а сказать не могу.
Рожал пару часов определения (на суд зрителей):
- multiprocess (много...что?) - вытесняющая многозадачность в пределах ОС. Одна задача - один процесс. Применительно к питону - один процесс питона с кодом.
- multithreading (многопоточность) - многозадачность в пределах одного процесса. Применительно к питону - один процесс питона и много потоков по одному потоку на задачу. С GIL - кооперативная многозадачность, без GIL - вытесняющая.
- async - кооперативная многозадачность в пределах одного процесса и одного потока. Последнее - но это не точно.
PS. кооперативная == конкурентная
GIL - кооперативная многозадачность, без GIL - вытесняющая.
Ну я бы не сказал, что с GIL многозадачность кооперативная. В обоих случаях переключение потоков выполняется планировщиком ОС, а он вытесняющий. Просто все потоки используют один блокируемый разделяемый ресурс, то бишь весь интерпретатор. Функции типа time.sleep перед началом отпускают GIL, поэтому отлично выполняются параллельно даже с GIL. Правильно написанные нативные библиотеки для CPU-bound задач (numpy) тоже отпускают GIL, так что они тоже работают параллельно даже с GIL.
Ну, я писал слишком фкрации, упуская ряд моментов:
1. да, вызов glibc отпускает GIL. НО - это не код именно питона.
2. А касабельно именно байт-кода питона, то сколько бы threads не было, в один момент времени обрабатывается одна команда именно питона (с включенным GIL). Вне зависимости от кол-ва ядер. Поэтому для именно питона multithread таки кооперативная многозадачность (параллельного выполнения не будет).
Пруфов не будет, это выжимка из того, что начитался и экспериментировал.
Поправьте, если ошибаюсь.
Ну по существу я согласен, что параллельного выполнения не будет, но по формулировке и терминологии - нет. Кооперативность/вытесняемость - это про то, как переключаются задачи, а не про возможность/невозможность параллельного исполнения. Блокирумые объекты можно захватывать при любом способе переключения задач, и исполнение не становится внезапно кооперативным. Более того, с точки зрения питон-программиста threading в любом случае вытесняющий - из питона нельзя захватить GIL так, что другие потоки не смогут исполняться, переключение в любом случае будет (раньше переключение происходило по счетчику опкодов, начиная с 3.2 - через заданные промежутки времени).
да, вызов glibc отпускает GIL. НО - это не код именно питона.
Не понял, причем тут glibc, но если вы имеете в виду код на питоне, то да, понятно, что изнутри интепретатора питона нельзя отпустить блокировку интерпретатора, можно только из C-кода, используя питоновое C API ( Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS
). Если вы вызываете glibc через ctypes, то ctypes добавляет такую обертку.
Ok, давайте так.
Надо различать multiprocess/multithread/async с кочки зрения ОС и с кочки зрения питон-машины.
Ну и зачем нужно - разнести питон-задачи по ядрам.
Давайте с кочки зрения питон-машины.
- multiprocess - здесь питон-машина вообще ни при чем. Это на уровне ОС.
- mutithread - здесь интересное. По существу это multiprocess в рамках питон-машины. Обращение к системным i/o помогает. А вот собственно байт-код питона во всех потоках - в одну очередь (ибо GIL). То есть тут и вытесняющая и кооперативная многозадачность в одном флаконе.
С выходом 3.13 появились варианты, здесь надо тыкать палочкой.
- async - теоретически... я вот фиг его знает. Такое впечатление, что multithread наоборот. Ну или костыль.
То есть тут и вытесняющая и кооперативная многозадачность в одном флаконе
Может, у нас разные определния кооперативности? Потому что я все еще не вижу ее тут.
Вот в async-подходе кооперативность есть - пока интерпретатор не дойдет до await
, переключения на другую корутину не произойдет, т.е. корутина управляет тем, когда с нее можно переключиться. При многопотоковости интерпретатор будет переключаться между потоками, даже если программист запустит в 10 потоках бесконечные циклы (даже если в них не будет блокирующих функций типа time.sleep).
async - теоретически... я вот фиг его знает. Такое впечатление, что multithread наоборот. Ну или костыль.
Напишу свое видение. Процесс - один, поток ОС - один. Внутри потока крутится написанный на питоне планировщик задач, он же eventloop, который переключается между задачами-корутинами. Поскольку целью было уменьшить накладные расходы на переключение и хранение состояния, корутины получились кооперативные - переключаться можно только когда корутина разрешит, и в этот момент записывается состояние, к которому можно потом вернуться.
Я бы сказал, что надо по слоям разложить.
Если вы в Питоне используете async-await это кооперативная многозадачнасть, реальзованная в коде.
Этот код выполняется в виртульной машине Питона, в которой есть свой планировщик задач. Там вытесняющая многозадачность.
Виртульная машина выполняется в операционной системе, там тоже свой планироващик и он тоже вытесняющий.
Всё это работает одновременно.
Этот код выполняется в виртульной машине Питона, в которой есть свой планировщик задач. Там вытесняющая многозадачность.
А это что за слой такой? Есть планировщик задач ОС (переключает процессы ОС и потоки ОС), есть async/await eventloop, например от asyncio (переключает корутины, выполняется в одном потоке ОС). Между ними нет никакого другого планировщика.
Наличие какого-то еще одного планировщика означало бы, что есть какие-то еще сущности, которые надо переключать, особые питоновские потоки, которые не являются потоками ОС. Но таких сущностей нет, потоки питона - это потоки ОС.
Наличие какого-то еще одного планировщика означало бы, что есть какие-то еще сущности, которые надо переключать, особые питоновские потоки, которые не являются потоками ОС. Но таких сущностей нет, потоки питона - это потоки ОС.
Потоки системные, а планировщик в виртуальной машине есть свой. Его даже можно настраивать.
https://docs.python.org/3/library/sys.html#sys.setswitchinterval
а планировщик в виртуальной машине есть свой. Его даже можно настраивать.https://docs.python.org/3/library/sys.html#sys.setswitchinterval
Это просто интервал, с которым один поток освобождает GIL. Если у вас в коде есть мьютексы, вы же не говорите, что сделали новый планировщик задач) Да и автор называет эту штуку не планировщиком, а переделкой GIL. Вот можно почитать про нее: https://mail.python.org/pipermail/python-dev/2009-October/093321.html
Это все, конечно, не по существу возражения, а больше по терминологии
мне, как старперу было проще и я застал ассемблер с концепцией прерываний. Как сейчас помню 21 прерывание с разными флагами :)
Тоесть в архитектуре харда было заложено некое прерывания чего то зачем то. И поняв, что же и зачем прерывалось, очень просто дойти до концепции асинхронности - это тоже самое, но на другом уровне.
И тут же рядом возникает вопрос мультипоточности: если что то зачем то прервалось( то есть ждет), то может это самое можно загрузить? А если можно загрузить, то как шарить данные между этими самыми загрузками того что ждет ответа от непонятно чего. Привет виндовз 3.11, там концепцию очень хорошо понимали, но реализовали как получилось. Сори за сарказм, но стартовая статья просто обязывает к этому.
Как старпер старперу:
- таки да, прерывания DOS это была многозадачность. Для некоторых прерываний (аппаратных) - вытесняющая. Да-да, DOS, вытесняющая многозадачность. О параллельном выполнении на многих ядрах тогда речь не шла.
- я больше скажу - оформить вытесняющую многозадачность можно было даже на i8080. Таймер вешается на NMI - и вуаля.
- но питон с этим своим GIL - это отдельная тема. Глобальный мютекс.
О параллельном выполнении на многих ядрах тогда речь не шла.
Сложно говорить о выполнении задач на многих ядрах, когда ядро в принципе одно и второе не предвидится в ближайшие годы.
прерывания DOS это была многозадачность.
И да и нет. Я таки повторю вопрос: что и зачем прерывалось. Ответив на этот нехитрый вопрос, все остальное станет очевидно.
- но питон с этим своим GIL - это отдельная тема. Глобальный мютекс.
В питоне придумали как вызвать обьект ядра ОС мутекс, который есть во всех POSIX-совместимых системах? Неужели? И главное - раньше то никто не знал про мутексы и семафоры!
В питоне придумали как вызвать обьект ядра ОС мутекс, который есть во всех POSIX-совместимых системах? Неужели? И главное - раньше то никто не знал про мутексы и семафоры!
Не. Python - это компьютер в компьютере. Как и эти ваши JavaScript, Java/Kotlin (JVM), PHP, C# и всё такое.
И GIL питона - это НЕ мютекс ядра ОС.
Нет, ну опосредованно код питона превращается в машинный.
Или постепенно (классика, интертрепатор) или оптом (JIT, только-что завезли).
Но это уже тюнинг. Расчитывать надо на "питон-машину".
JIT, только-что завезли
и эти люди дотнет критиковали что там JIT лет 20 работает не так :)
Ну да ок, напишу развернуто про асинхронность-многопоточностсть-многопроцессность.
Для понимая мне думается имеет смысл откатиться лет на 50-70 назад и потом осмысливать концепции с эволюцией харда и софта.
----
В 70е годы большинство пользовательских компов имели одноядерный проц. Он, умел(и до сих пор многоядерные процы умеют)очень быстро изменять данные в ОЗУ/регистрах процессора - большинство команд ассеблера как раз про то чтобы взять какие то данные из ОЗУ(возможно положить в реестры) и изменить их, шина данных между процом и ОЗУ всегда была и есть самой быстродейственной среди всех остальных шин данных. Но неожиданно оказалось, что необходимо не только изменить данные в ОЗУ, но и еще принимать/передавать данные в/из так называемые устройства ввода-вывода(IO ports), которые включают в себя монитор, винт, клавиатуру, мышу, сетевую карту и так далее. И тут внезапно, вся эта обвеска работает на порядки медленей чем проц/память - как было в 70е так и есть до сих пор. И наш одноядерный проц выдает команду на это самое устройство ввода/вывода и потом.... ничего не делает, то есть ждет ответа от медленного устройства. Вот команда "скоммуницируй с медленным устройстов ввода/вывода" и назысается прерыванием, потому что она буквально прерывает работу процессора по непрерывному изменению данных в ОЗУ и ждет пока внешнеее устройство отдуплит. Это неэффективно, время ожидания можно занять чем-то другим, тоесть выполнением процессором других потоков команд, а когда внешнее устройство отдуплится то и можно продолжать выполнять предыдущий.
----
Тут же возникает концепция "пула процессов", которые становятся в очередь ожидания IO операций и попутно оказалось что можно пулом процессов решать не только проблему асинхронности, но об этом ниже.
Тоесть синтаксический сахар в виде async/await предназачен для эффективного использования CPU "пока внешние устройства дуплят" и точка.
Почти.
скобках отметим что тут нет никакой синхронизации данных потому что... нечего синхронизировать. Каждый процесс работает со своими данными.
----
Затем прошло еще много лет и процессоры стали многоядерными - тоесть они могут выполнять несколько потоков команд одновременно. Каждый поток команд работает со своими данными и называтся процессом. А если в рамках одго процесса есть несколько потоков команд, которые шарят общие данные - то их так и назвали "потоки".
Максимально эффективная утилизация CPU - это "процесс на ядро", но процессы опять таки взаимодействуют с IO устройствами и некоторые ядра процессора могут простаивать в ожидании медленных внешних устройств. Это нехорошо, но пул процессов придумали раньше и почему бы не расширить эту концепцию для многоядерного железа. Расширили. И да никаких блокировок тут не нужно, потому что у каждого процесса свои данные.
----
Гдето в 50е года прошлого века появились алгоритмы, которые обращаются к одним и тем же данным параллельно в рамках одого процесса, тоесть несколькими потоками. Но пока процессоры были одноядерные, то эти алгоритмы были не очень актуальны - один проц, один процесс в одну единицу времени, ок с оптимизацией IO операций. Но с появлением многоядерных процов, проблема доступа к конкуррентым данным стала ппц насколько актуальной. И появились так называемые "примитивы синхронизации", тоесть некоторые механизмы, которые преднназначены для упорядоченного чтения/записи данных в общую область памяти. Но появилась проблемка: никто не парится тем чтобы создавать количество потоков, адекватное количество ядер компа. Если задачу проще решить созданием тысячи потоков... дык почему бы не создать тысячу. Или 10 тыщ. И тут оказалось, что уже есть пул потоков для IO операций. Почему бы его не зареюзать для могопоточности? И да, зареюзали. И да, отсюда вот такие статьи как выше. Несмотря на своершенно разные концепции - просто удобный инструмент дял решения совершенно разных проблем. И усе. Спасибо что прочитали.
----
Если камент наберет 10 лайков, сделаю из него статью, таки лонг райт получился.
Недавно на очередном собесе вопрос: "Чем отличаются multiprocess, multithreading и async?"А я как собака - знать знаю, а сказать не могу.
multiprocess vs multithreading это про память. Треды сидят в одном пространстве, а процессы в своих изолированных.
Треды и процессы могут как IO-bound так и CPU-bound задачи, хотя есть нюансы с GIL. Асинхронность она только для IO-bound.
Накладные расходы больше всего у процессов, меньше у тредов и совсем немного для асинхронных.
Ваш бы ответ не принял, особенно за то, что асинхронность одно-поточная. Но я такой вопрос задавать на собеседовании стал бы только если у человека есть опыт в этом направлении. Тема не самая простая и без желания в ней глубоко разобраться или реальной практики, вряд ли услышите что-то разумное.
Как интегрировать синхронный код в асинхронный. Инструкция