Comments 12
Как-то пытался объяснять последствия изоляции всего и вся GIL-ом в python, и накидал маленький пример для наглядности:
https://gist.github.com/sebres/230c4bfafc36c99074202dc59b194a95
Если что, в приведенном примере ("тупой" инкремент в цикле 100M раз) для 4-х потоков, python медленнее вашего любимого языка без GIL tcl в 15 раз (35 сек. vs. 2.3 сек.), а однопоточно более чем в 3 раза.
И это при том, что собственно код исполнения не "пересекается" нигде - не использует общих (shared) объектов.
CPU-bound вычисления в питоне выполняются однопроцессно.
Странный пример.
Примерно как дергать API для уже известного значения (только в данном случае переключать поток)
Что, простите?!
CPU-bound вычисления в питоне выполняются однопроцессно
Нет! Это издержки GIL. Если лень смотреть в исходники, просто см. результат для многопоточного исполнения (натяжение "ручника" зависит от количества потоков, что при N threads < M CPU core однозначно указывает на overhead от "чрезмерной" блокировки).
Странный пример...
Пример как пример... Можно попробовать что-нибудь другое (не "CPU-bound"), результат не изменится. А можно попробовать что-нибудь без GIL (Iron, PyPy STM, хоть тот же PoC Сэма) и узреть разницу.
только в данном случае переключать поток
Никакой поток тут нигде не переключается (напрямую)... каждый поток исполняет собственный изолированный код (с собственным циклом и своими переменными - с полностью независымыми объектами PyObject
и PyVarObject
) и context-switch если и происходит, то исключительно на lock-ах в GIL (совершенно не нужном здесь, т.к. пересечений и shared references нет совсем).
Пример собственно это и показывает.
Ну попробуйте не CPU bound сделать - жажду увидеть 4 потока медленнее одного, продемонстрируйте...
Издержки издержками, но в текущей архитектере CPU bound вычисления выполняются однопроцессно или мультипроцессно, использовать треды для этого - это просто потеря производительности, тк на ядра они не раскидываются.
Зная это писать такой код и говорить какой питон плохой - странно. Ведь это просто неправильно использовать инструмент, о чем вам явно говорит документация:
https://docs.python.org/3/library/threading.html
If you want your application to make better use of the computational resources of multi-core machines, you are advised to use multiprocessing
or concurrent.futures.ProcessPoolExecutor
.
Ну попробуйте не CPU bound сделать - жажду увидеть 4 потока медленнее одного, продемонстрируйте...
Любой код где исполнение касается GIL-защищенных объектов будет сваливаться в бесконечный lock. Если вы говорите про "абстрактный" C-модуль, где всё вычисление происходит исключительно в С (без дерганья GIL-related primitives), и только результат помещается в питоний объект, то вы ошиблись статьей - здесь про архитектуру python, а не про вызов "сторонних" библиотек из питона. И overhead будет тем больше, чем больше обвязанных GIL объектов ваш код затронет (т.е. чем короче длинна той стрелки в связке python --> C --> python
, и чем короче исполнение собственно в C-модуле).
в текущей архитектере CPU bound вычисления выполняются однопроцессно или мультипроцессно...
Зная это писать такой код и говорить какой питон плохой - странно.
У вас простите причинно-следственная связь нарушена. От слова совсем.
При этом критику конкретного недостатка дизайна архитектуры СPython, совершенно заслуженную кстати, вы почему то пытаетесь оправдать наличием в документации сносок, собственно и возникших в результате нахождения тех концептуальных недостатков GIL и иже с ним (заметьте не python, а конкретной реализации). При том что та же критика слышна и из рядов разработчиков языка, причем этот вопрос на моей памяти поднимался неоднократно.
Ну и простейший пример (на подумать) - если добавить флаг состояния объекта типа IsThreadShared
, и исключить блокировку GIL-ом на объектах, у которых оно false
(а еще лучше у всего bytecode и части evaluation stack, которая затронута тем кодом), то тот пример будет выполнен в 3 раза быстрее однопоточно и 15 раз быстрее в 4 потока. Тоже самое будет если просто тупо "выключить" GIL (просто в качестве теста), т.е. на стадии компиляции python переопределив stable ABI PyGILState_Ensure
, PyGILState_Release
, PyGILState_Check
и иже с ними.
Тема же multiprocessing vs. multithreaded совершенна ортогональна тут и у меня простите здесь обсуждать это с вами нет ни малейшего желания.
Интересно причинно-следственную связь в появлении GIL проследить. Решили рулить память с помощью счетчика ссылок, и обошли проблемы синхронизации GILом? Или что то другое послужило такому решению?
Насколько я понимаю, изначально целевой аудиторией питона были скрипты для пакетной обработки данных, а не сервисы массового обслуживания. Питон позиционировался как скриптовый язык, помогающий склеивать код разных библиотек, написанным на чем угодно, в одно приложение. Компромиссным решением было дать по умолчанию всем доступ к общей памяти. Но как эти библиотеки работают с многопоточностью знают только они сами. Наверно первое, что приходит в голову в таком случае, это перед вызовами делать блокировку.
Всё равно не понял, каким образом GIL решает проблему гонки для неатомарных операций вроде += 1
Если блокировка захватывается на время выполнения опкода и может быть освобождена между опкодами, то где решение?
Это атомарный опкод с точки зрения Python. UNARY_POSITIVE. Но это не атомарная операция с точки зрения машинных инструкций.
В том большом switch, не может произойти взятия/освобождения GIL пока опкод не выполнится полностью.
Если бы GIL небыло, то выполняя UNARY_POSITIVE в одном треде, ОС могла бы прервать его выполнение и тем самым нарушив атомарность.
При GIL, даже если ОС переключит поток во время выполнения опкода, GIL остаётся заблокированным и никакой другой поток не сможет его взять и нарушить наши данные. После возврата ОС на данный поток, мы продолжим выполнять нашу атомарную операцию. И на следующей итерации бесконечного цикла будем проверять надо ли ещё кому-то GIL или нет.
Спасибо за статью - познавательно. Однако, что касается сути вопроса - а разве не общепринят подход: multithreading - для кода ввода/вывода, multiprocessing - для CPU-bound?
Глобальная блокировка интерпретатора (GIL) и её воздействие на многопоточность в Python