CPU, Memory Models, Concurrency, Multiprocess, Multithreading и Async. Часть 17. Python.
Приступаем к препарированию языков программирования. Начнем с одного из самых популярных языков.
Какие примитивы доступны в Python для конкурентности и параллелизма?
- процессы (пакет multiprocessing)
- потоки (пакет multithreading)
- корутины (asyncio)
- зеленые потоки (gevent)
- Stackless Python
Что еще есть в Python важного в контексте конкуррентности? Великий и ужасный Global Interpreter Lock он же GIL.
Что такое GIL?
Если простым языком это мьютекс не позволяющий двум и более потокам (они в Python по умолчанию мапятся на потоки ОС 1к1, см. предыдущий пост) параллельно исполнять код интерпретатора. Как следствие, программа не может достичь параллелизма и утилизирует только 1 ядро CPU.
Почему потокам запрещено исполнять параллельно код интерпретатора?
В большинстве интерпретаторов код написан not thread safe и поэтому если тз такого интерпретатора убрать GIL можно словить segfault и прочие артефакты.
Зачем нужен GIL? Какие плюсы?:
- Его наличие позволяет писать быстрый и простой код разработчикам интерпретатора, меньше обмазываться мьютексами внутри него. И как следствие наши программы на Python также становятся более быстрыми.
- Наличие GIL позволяет легче и проще подключать чужой код( в большинстве случаев непотокобезопасный) в виде расширений (для CPython на языке С например). Тоже самое справедливо и при разработке собственных расширений.
Давайте теперь пройдем разберемся по примитивам и рассмотрим как на них влияет GIL
🔵Потоки
Влияние значительное, отсутствие параллелизма в случае CPU задач. Для IO задач позволяют выжать больше пропускной способности. Так как потоки мапятся 1 к 1 на потоки ОС есть накладные расходы (см пост выше).
🔵Процессы
Не испытывают влияния GIL. Используя многопроцессность мы можем использовать все ядра и добиться параллелизма. Имеют еще больше накладных расходов по сравнению с потоками, существуют техники оптимизации (например copy on write для уменьшения расходов памяти)
🔵Корутины asyncio и зеленые потоки gevent.
Для CPU задач производительность аналогична потокам. Для IO задач предпочтительнее чем потоки. Корутины создаются и управляются на уровне программы, ОС ничего не знает о них и переключение между ними происходит быстрее, чем между потоками (которые управляются ОС). Важно отметить что корутины и зеленые потоки реализуют кооперативную многозадачность, то есть наличие переключений между задачами зависит от того как написан код. Я не раз видел асинхронный код который блочил единственный тред и программа зависала навсегда.
🔵Stackless Python
В рамках этой реализации введен примитив микропотоков, по сути это тоже самое что корутины и зеленые потоки с одним важным ньюансом - в stackless python реализован scheduler который переключает задачи выделяя каждой время. Получаем вытесняющую многозадачность аналогичную той что есть в операционной системе. В этой реализации Python также встроен GIL.
Можно ли жить без GIL?
Есть реализации Python без GIL, утилизирующие ядра по максимуму. Но практических примеров использования я не знаю, напишите в комментариях если встречали. Еще подмечу что корутины и зеленые потоки не умеют работать с non-GIL реализациями. Посмотрим что будет когда GIL окончательно выпилят. Судя по новостям эта задача в фокусе у Core Team.
Как выжимать максимум из железа на Python? Мой опыт
- Создавать процессы чтобы утилизировать ядра. Пример: все популярные application servers умеют в мультипроцессорный режим.
- Не создавайте процессов больше чем ядер.
- Используйте пул потоков чтобы контролировать ресурсы ОС.
- Если у вас приложение связанное с IO то имеет смысл использовать корутины / зеленые потоки, фреймворки на их основе (привет FastAPI)
- Если работаете с ML то нужны не только процессы, но и умение влезть в сишку чтобы написать что-то оптимальное, так как там много CPU задач.
На этом всё, спасибо что читали😊 Расскажите в комментариях вашу историю знакомства с конкурентностью в Python?
Post #341
2.18K
- 🔥 14
- 👍 6
- ❤ 2