В своем докладе Григорий проведет краткий экскурс в историю потоков и расскажет, зачем был создан GIL. Будут рассмотрены практические вопросы многопоточности в Python и способы работы с GIL.
Warning В докладе много упрощений и допущений, особенно это касается многопроцессорных систем. Точность принесена в жертву простоте изложения, чтобы не превратить обзорный доклад с плюшками в сухую и бесполезную лекцию.
Упрощение до предела У компьютера есть память и процессор. Все что компьютер делает, - это берет байтики из памяти, грустно на них смотрит и меняет на другие байтики в памяти. И все.
Первые шаги Первые компьютеры и первые операционные системы были очень простые: они выполняли только одну программу за раз. Все программы загружались в общую память и передавали друг другу управление – кооперативная многозадачность.
И первые хотелки Пользователи хотели выполнять несколько программ одновременно. И программисты создали абстракцию - процесс. С кооперативной многозадачностью. Все программы загружались в общую память и передавали друг другу управление.
Но программисты не смогли Программисты не смогли писать программы без ошибок - слишком дорого. И когда программа зависала, она не передавала никому управление, и зависали все запущенные программы во главе с операционной системой. … А еще программы портили память. Надо было что-то делать.
Вытесняющая многозадачность Раз в 20 миллисекунд процесс засыпает, и просыпается мафия операционная система, которая обозревает спящие процессы и решает, кому из них просыпаться. Зависшие процессы больше не останавливают операционную систему. Правильно написанная программа большую часть времени спит, ожидая чего-нибудь интересного. Ее разбудят.
Виртуальная память В CPU была добавлена функция обмана программ. Каждая программа считает, что у нее есть вся память от 0 до бесконечности*. А при записи и чтении соответствующий кусочек памяти записывается и читается в физическую память. * Для 32-битных систем бесконечность - от 2 до 3 гигабайт, в зависимости от настроек.
Но программисты снова не смогли В процессе написания больших и сложных программ возник ряд вопросов: ● Если программа читает файл, то она ждет, пока операционная система вернет управление. ● Асинхронные функции и алгоритмы слишком сложны и не всегда доступны. ● Запуск дополнительных процессов слишком долгий. * * Для Windows :)
Поток ● Это процесс без виртуализации памяти :) ● Засыпает и просыпается так же, как раньше засыпали и просыпались процессы. ● Читает и пишет ту же виртуальную память, что и остальные потоки одного процесса. ● Быстро стартует. ● При создании процесса создается первый, он же “основной” поток.
Но программисты снова не смогли Вам рассказывали, что проблема многопоточности - в одновременном доступе к памяти? Вас обманули. Компьютер ничего не делает “одновременно” - он играет в пошаговую стратегию. * ● Проблема потоков в том, что они засыпают и просыпаются в неожиданные моменты времени. * Многоядерные играют в несколько непересекающихся пошаговых стратегий.
Совсем неожиданные Память Поток 2 Поток 1 Поток работает Поток работает Поток работает Поток спит Поток спит Поток спит Общая память потоков Общая память потоков Общая память потоков Указатель не 0? Читаем по адресу Обнулить :) Указатель → адрес Указатель → 0 Указатель → 0
И что тогда делает Lock в GIL? Если потоки и без GIL не выполняются одновременно, то как GIL может, цитируя документацию, “prevent multiple native threads from executing Python bytecodes at once (одновременно)”?
И что тогда делает Lock в GIL? Если потоки и без GIL не выполняются одновременно, то как GIL может, цитируя документацию, “prevent multiple native threads from executing Python bytecodes at once (одновременно)”? А никак. Он не это делает.
От чего защищает GIL? От того, что второй поток проснется неожиданно для первого и поменяет структуры в памяти интерпретатора. Проснувшийся первый поток будет очень удивлен. Вплоть до падения.
Как он это делает? Тривиально. Программа не может предотвратить засыпание своих потоков, но она может контролировать, когда они проснутся. Python принудительно погружает все потоки, кроме текущего, в сон. Они спят и ждут светлого будущего.
Как он это делает? Тривиально. Программа не может предотвратить засыпание своих потоков, но она может контролировать, когда они проснутся. Python принудительно погружает все потоки, кроме текущего, в сон. Они спят и ждут светлого будущего GIL.
Как он это делает? Тривиально. Программа не может предотвратить засыпание своих потоков, но она может контролировать, когда они проснутся. Python принудительно погружает все потоки, кроме текущего, в сон. Они спят и ждут светлого будущего GIL. А когда Python понимает, что текущий поток уже слишком долго работает в гордом одиночестве, - он усыпляет его в ожидаемом месте и будит один из своих спящих потоков.
И как ему это удается? Операционная система усыпляет и будит потоки неожиданно, и ей нет дела до того, что Python хочет свой текущий поток усыпить в ожидаемое время. Что же делать?
И как ему это удается? Операционная система усыпляет и будит потоки неожиданно, и ей нет дела до того, что Python хочет свой текущий поток усыпить в ожидаемое время. Что же делать? Нужно интегрироваться в операционную систему и поменять логику, по которой она усыпляет и будит потоки!
И как ему это удается? Операционная система усыпляет и будит потоки неожиданно, и ей нет дела до того, что Python хочет свой текущий поток усыпить в ожидаемое время. Что же делать? Нужно интегрироваться в операционную систему и поменять логику, по которой она усыпляет и будит потоки! Плохая идея.
И как ему это удается? Операционная система усыпляет и будит потоки неожиданно, и ей нет дела до того, что Python хочет свой текущий поток усыпить в ожидаемое время. Что же делать? Нужно, чтобы текущий поток уснул сам. В ожидаемое время.
Когда можно засыпать? ● Нельзя, чтобы другой поток проснулся, когда мы находимся внутри C-кода интерпретатора. ● Автор Python решил, что текущий поток будет засыпать раз в 100 тиков, где тик примерно соответствует одной инструкции интерпретатора. ● sys.getcheckinterval, sys.setcheckinterval
Версия 3.2 ● Потоки спят и ждут GIL не вечно, а 5 мс. ● Проснувшийся по таймауту поток обижается, выставляет флаг “хочу GIL” и снова засыпает. ● Текущий поток в конце каждого тика проверяет наличие флага “хочу GIL” и, если таковой есть, будит один из ожидающих GIL потоков и засыпает. ● sys.getswitchinterval, sys.setswitchinterval
Итак, что же делает GIL? ● В нормальных условиях потоки засыпают и просыпаются по велению операционной системы - часто и неожиданно. ● GIL запрещает всем потокам Python, кроме текущего, просыпаться неожиданно. ● Текущий поток засыпает и просыпается по велению операционной системы. Но когда Python решает, что пора переключаться, - текущий поток засыпает сам.
И где же здесь проблема? Мы запустили 10 потоков на скачивание файлов. Они не смогут просыпаться часто в случайный момент времени и будут качать с той же скоростью, как если бы были запущены последовательно?
И где же здесь проблема? Мы запустили 10 потоков на скачивание файлов. Они не смогут просыпаться часто в случайный момент времени и будут качать с той же скоростью, как если бы были запущены последовательно? А вот и нет. В Python все предусмотрено.
Механизм поднятия GIL GIL защищает от того, что поток A неожиданно уснет, поток B неожиданно проснется и поменяет байтики внутри интерпретатора, которые поток А ну никак не ожидает видеть измененными. Но если в потоке вызвана функция операционной системы для приема данных по сети - это не код python, и он не может поменять байтики в интерпретаторе.
Механизм поднятия GIL Когда Python вызывает функцию операционной системы или внешней библиотеки, он отключает механизм GIL. А после того как функция вернет управление, снова включает его. Это называется “поднять GIL” и “опустить GIL”.
Промежуточный вывод Если у нас одноядерный процессор, GIL ничем не мешает нашим потокам и приносит пользу: ● Код, не имеющий отношения к интерпретатору, засыпает и просыпается, когда ему надо, и не ждет ничего лишнего. ● Python-код засыпает и просыпается аккуратно, чтобы ничего не повредилось.
Физическая и логическая защита GIL защищает память от повреждений и делает тики атомарными. Но тики — довольно мелкая единица языка: if a > 0: a = 10 Здесь от двух тиков и более.
А если много ядер? ● Если у вас много ядер и вы хотите максимизировать скорость выполнения именно Python кода — значит, у вас очень редкий случай :) ● NumPy и SciPy поднимают GIL на долгих операциях (работа с матрицами). ● Используйте библиотеки для расчетов ● Используйте процессы.
Можно кидать помидоры Рассказывал, показывал и махал руками Григорий Петров. Со мной можно связаться: [email protected] www.facebook.com/grigoryvp Вопросы?