Pull to refresh

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 или нет.

Так, спустя много времени, мне указали на ошибочность моего рассуждения. UNARY_POSITIVE это не то что я думал. += это INPLACE_ADD и для выполнения a += 1 будет использовано минимум 3 опкода.

Так что мой ответ выше - бред и не правда.

Спасибо за статью - познавательно. Однако, что касается сути вопроса - а разве не общепринят подход: multithreading - для кода ввода/вывода, multiprocessing - для CPU-bound?

Sign up to leave a comment.