Как проще всего избежать ограничений GIL? | OTUS

Как проще всего избежать ограничений GIL?

Python_Deep_8-5020-a10ed2.10_site.png

Многие из вас знают, что в Python есть GILGlobal Interpreter Lock, тот самый, который не даёт запускать несколько потоков и нагружать ядра процессора. Отчасти это так, но за GIL в Python скрывается очень много всего. И вот несколько фактов о нём:

GIL будет всегда. Но это не точно. — Захват GIL – одна из первых инструкций, которая выполняется в начале работы вашего кода. — Блокировка переключается каждые 100 инструкций байт-кода до версии 3.2. — Однако в Python до версии 3.2 у нас была возможность управлять этой цифрой. После – нет. — Всё потому, что в Python 3.2 и выше GIL был очень сильно переписан. Теперь блокировка переключается по времени. — Потоки соревнуются за захват GIL, у некоторых из них, например, тех, которые активно занимаются вводом\выводом, это получается чаще. — До версии 3.2 есть тонкости обработки сигналов при многопоточном коде – ваш код иногда может не получить сигнал от ОС. — Планированием выполнения потоков занимается ОС, а Python только говорит ей, как ему бы хотелось. — Вы можете обойти ограничения в своём коде при помощи multiprocessing. — Или написав своё Python C Extension.

Рассмотрим один из самых очевидных способов избежать ограничений GIL’а при выполнении CPU-intensive задач.

Модуль multiprocessing

Запускай по процессу на ядро и вперёд! К сожалению, очевидный способ не значит, что простой. Допустим, запускается процесс, который fork’ает дочерний процесс, но перед этим загружает в память какой-нибудь большой read-only кэш, необходимый этим процессам для функционирования.

Вот это класс! Казалось бы, за счёт copy-on-write (предполагаем Linux) оба процесса будут видеть один и тот же кэш, но дублировать его в памяти не придётся. В top (ну или ps) увидим, что у процессов RSS (Resident Set Size) меньше VMS (Virtual Memory Size). А если кто-то из них и решит туда записать, то ОС скопирует в его адресное пространство только нужные страницы.

Всё так, но есть нюансы

В Python управление памятью осуществляется с помощью reference counting. И даже простой цикл по списку, например, вызывает увеличение счётчика ссылок находящихся в нём объектов. То есть, скорее всего, наш большой кэш не останется надолго в общей памяти, а быстро скопируется в адресные пространства процессов.

Чтобы этого избежать нужно положить кэш в shared memory, то есть разделяемую память, которая является общей для обоих процессов. Multiprocessing даёт нам такую возможность, но выбор опций достаточно скудный: можно создать одно значение или массив. Для чего-то более сложного придётся использовать mmap, но программировать это будет ещё сложнее.

Есть вопрос? Напишите в комментариях!

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто