Как известно, в основной реализации Питона CPython (python.org) используется Global Interpreter Lock (GIL). Эта штука позволяет одновременно запускать только один питоновский поток — остальные обязаны ждать переключения GIL на них.
Коллега Qualab недавно опубликовал на Хабре бойкую статью, предлагая новаторский подход: создавть по субинтерпретатору Питона на поток операционной системы, получая возможность запускать все наши субинтерпретаторы параллельно. Т.е. GIL как бы уже и не мешает совсем.
Идея свежая, но имеет один существенный недостаток — она не работает…
Позвольте мне сначала рассмотреть GIL чуть подробней, а потом перейдем к разбору ошибок автора.
GIL
Тезисно опишу существенные для рассмотрения детали GIL в реализации Python 3.2+ (более подробное изложение предмета можете найти тут).
Версия 3.2 выбрана для конкретики и сокращения объема изложения. Для 1.x и 2.x отличия незначительны.
- GIL, как следует из названия — это объект синхронизации. Предназначен для блокирования одномоментного доступа к внутреннему состоянию Python из разных потоков.
- Он может быть захвачен каким-либо потоком или оставаться свободным (незахваченным).
- Одновременно захватить GIL может только один поток.
- GIL один единственный на весь процесс, в котором выполняется Python. Еще раз подчеркну: GIL спрятан не в субинтерпретаторе или где-то еще — он реализован в виде набора static variables, общими для всего кода процесса.
- С точки зрения GIL каждому потоку, выполняющему Python C API вызовы, должна соответствовать структура PyThreadState. GIL указывает на один из PyThreadState (работающий) или не указывает ни на что (GIL отпущен, потоки работают независимо и параллельно).
- После старта интерпретатора единственная операция, позволенная над Python C API при незахваченном GIL — это его захват. Всё остальное запрещено (технически безопасен также Py_INCREF, Py_DECREF может вызвать удаление объекта, что может вызвать бесконтрольное незащищенное одновременное изменение того самого внутреннего состояния Python, которое и пытается предотвратить GIL). В DEBUG сборке проверок на неправильную работу с GIL больше, в RELEASE часть отключена для повышения производительности.
- Переключается GIL по таймеру (по умолчанию 5 мс) или явным вызовом (
PyThreadState_Swap, PyEval_RestoreThread, PyEval_SaveThread, PyGILState_Ensure, PyGILState_Release и т.д.)
Как видим, запускать одноременное параллельное выполнение кода можно, нельзя при этом делать вызовы Python C API (это касается выполнения кода написанного на питоне тоже, естественно).
При этом «нельзя» означает (особенно в RELEASE сборке, используемой всеми) что такое поведение нестабильно. Может и не сломаться сразу. Может на этой программе вообще работать замечательно, а при небольшом безобидном изменении выполняемого питоновского кода завершаться с segmentation fault и кучей побочных эффектов.
Почему субинтепретаторы не помогают
Что же делает коллега Qualab (ссылку на архив с кодом можете найти в его статье, исходник я продублировал на gist: gist.github.com/4680136)?
В главном потоке сразу же отпускается GIL через PyEval_SaveThread(). Главный поток больше с питоном не работает — он создает несколько рабочих потоков и ждет их завершения.
Рабочий поток захватывает GIL. Код вышел странноватым, но сейчас это не принципиально. Главное — GIL зажат у нас в кулаке.
И сразу же параллельное исполнение рабочих потоков превращается в последовательное. Можно было и не городить конструкцию с субинтерпретаторами — толку от них в нашем контексте ровно ноль, как и ожидалось.
Не знаю, почему автор этого не заметил сразу, до опубликования статьи. А потом долго упорствовал, предпочитая называть черное белым.
Вернуться к параллельному исполнению просто — нужно отпустить GIL. Но тогда нельзя будет работать с интерпретатором Питона.
Если всё же наплевать на запрет и вызывать Python C API без GIL — программа сломается, причем не обязательно прямо сразу и не факт что без неприятных побочных эффектов. Если хотите выстрелить себе в ногу особенно замысловатым способом — это ваш шанс.
Повторюсь опять: GIL один на весь процесс, не на интерпретатор-субинтерпретатор. Захват GIL означает, что все потоки выполняющие питоновский код приостановлены.
Заключение
Нравится GIL или не очень — он уже есть и я настоятельно рекомендую научиться правильно с ним работать.
- Либо захватываем GIL и вызываем функции Python C API.
- Или отпускаем его и делаем что хотим, но Питон трогать в этом режиме нельзя.
- Параллельная работа обеспечивается одновременным запуском нескольких процессов через multiprocessing или каким другим способом. Детали работы с процессами выходят за рамки этой статьи.
Правила простые, исключений и обходных лазеек нет.
Автор: svetlov