577 подписчиков

GIL или потоки и процессы на примерах python

229 прочитали

Мы исследуем глобальную блокировку интерпретатора Python и узнаем, как она влияет на многопоточные программы.

Давайте прочитаем исходный код интерпретатора CPython и выясним, что именно такое GIL, почему он есть в Python и как он влияет на ваши многопоточные программы. Я покажу примеры, которые помогут вам разобраться в GIL. Вы научитесь писать быстрый и потокобезопасный Python, а также научитесь выбирать между потоками и процессами.

Мы исследуем глобальную блокировку интерпретатора Python и узнаем, как она влияет на многопоточные программы....

(Для удобства я описываю здесь только CPython, а не Jython, PyPy или IronPython. CPython - это реализация Python, которую в подавляющем большинстве используют работающие программисты.)

static PyThread_type_lock interpreter_lock = 0;

Эта строка кода находится в ceval.c, в исходном коде интерпретатора CPython 2.7. Комментарий Гвидо ван Россума «Это GIL» был добавлен в 2003 году, но сама блокировка восходит к его первому многопоточному интерпретатору Python в 1997 году. В системах Unix PyThread_type_lock является псевдонимом для стандартной блокировки C, mutex_t. Он инициализируется при запуске интерпретатора Python:

 Весь код C в интерпретаторе должен удерживать эту блокировку при выполнении Python.  Гвидо сначала построил Python таким образом, потому что он прост, и каждая попытка удалить GIL из CPython стоила однопоточным программам слишком большой производительности, чтобы оправдать выгоды от многопоточности.
Весь код C в интерпретаторе должен удерживать эту блокировку при выполнении Python. Гвидо сначала построил Python таким образом, потому что он прост, и каждая попытка удалить GIL из CPython стоила однопоточным программам слишком большой производительности, чтобы оправдать выгоды от многопоточности.

Воздействие GIL на потоки в вашей программе достаточно простое, чтобы вы могли написать принцип на своей ладони: «Один поток выполняет Python, а N других ждут ввода-вывода». Потоки Python также могут ждать threading.Lock или другого объекта синхронизации от модуля threading; Считайте, что потоки в этом состоянии тоже «спят».

Мы исследуем глобальную блокировку интерпретатора Python и узнаем, как она влияет на многопоточные программы....-3

Когда переключаются потоки?

Каждый раз, когда поток начинает спать или ожидает сетевого ввода-вывода, есть шанс, что другой поток воспользуется GIL и выполнит код Python. Это совместная многозадачность. CPython также имеет вытесняющую многозадачность: если поток работает без прерывания для 1000 инструкций байт-кода в Python 2 или работает 15 миллисекунд в Python 3, тогда он отказывается от GIL и может работать другой поток. Думайте об этом как о временном разрезе в былые времена, когда у нас было много потоков, но один процессор. Я подробно рассмотрю эти два вида многозадачности.

Когда он начинает задачу, такую ​​как сетевой ввод-вывод, которая имеет длительную или неопределенную продолжительность и не требует запуска какого-либо кода Python, поток отказывается от GIL, чтобы другой поток мог взять его и запустить Python. Такое вежливое поведение называется совместной многозадачностью и допускает параллелизм; многие потоки могут ждать разных событий одновременно.

Допустим что два потока каждый соединяют сокет:

Только один из этих двух потоков может выполнять Python одновременно, но как только поток начинает соединение, он отбрасывает GIL, чтобы другой поток мог работать.  Это означает, что оба потока могут ожидать одновременного подключения своих сокетов, и это хорошо.  Они могут выполнить больше работы за то же время.
Только один из этих двух потоков может выполнять Python одновременно, но как только поток начинает соединение, он отбрасывает GIL, чтобы другой поток мог работать. Это означает, что оба потока могут ожидать одновременного подключения своих сокетов, и это хорошо. Они могут выполнить больше работы за то же время.

Давайте откроем коробку и посмотрим, как поток Python фактически отбрасывает GIL, ожидая установления соединения в socketmodule.c:

Py_BEGIN_ALLOW_THREADSmacro - это то место, где поток отбрасывает GIL;  он определяется просто как: 
 PyThread_release_lock (интерпретатор_блок);
Py_BEGIN_ALLOW_THREADSmacro - это то место, где поток отбрасывает GIL; он определяется просто как: PyThread_release_lock (интерпретатор_блок);

И, конечно же, Py_END_ALLOW_THREADS восстанавливает блокировку. Поток может заблокироваться в этом месте, ожидая, пока другой поток освободит блокировку; как только это произойдет, ожидающий поток возвращает GIL и возобновляет выполнение вашего кода Python. Вкратце: пока N потоков заблокированы при вводе-выводе по сети или ожидают повторного получения GIL, один поток может запускать Python.

Ниже приведен полный пример, в котором используется совместная многозадачность для быстрого получения множества URL-адресов. Но перед этим давайте сравним кооперативную многозадачность с другими видами многозадачности.

Вытесняющая многозадачность

Поток Python может добровольно освободить GIL, но также может заблаговременно захватить GIL.

Давайте вернемся и поговорим о том, как выполняется Python. Ваша программа выполняется в два этапа. Во-первых, ваш текст Python компилируется в более простой двоичный формат, называемый байт-кодом. Во-вторых, основной цикл интерпретатора Python, функция, ласково названная PyEval_EvalFrameEx (), считывает байт-код и выполняет инструкции в нем одну за другой.

Пока интерпретатор проходит через ваш байт-код, он периодически сбрасывает GIL, не спрашивая разрешения потока, код которого он выполняет, поэтому другие потоки могут работать:

По умолчанию интервал проверки составляет 1000 байт-кодов.  Все потоки запускают один и тот же код, и блокировка периодически снимается с них одним и тем же способом.  В Python 3 реализация GIL более сложна, и интервал проверки - это не фиксированное количество байт-кодов, а 15 миллисекунд.  Однако для вашего кода эти различия несущественны.
По умолчанию интервал проверки составляет 1000 байт-кодов. Все потоки запускают один и тот же код, и блокировка периодически снимается с них одним и тем же способом. В Python 3 реализация GIL более сложна, и интервал проверки - это не фиксированное количество байт-кодов, а 15 миллисекунд. Однако для вашего кода эти различия несущественны.

Безопасность потоков в Python

Если поток может потерять GIL в любой момент, вы должны сделать свой код потокобезопасным. Однако программисты на Python думают о безопасности потоков иначе, чем программисты на C или Java, потому что многие операции Python являются атомарными.

Пример атомарной операции - вызов sort () для списка. Поток нельзя прервать в середине сортировки, а другие потоки никогда не видят частично отсортированный список и не видят устаревшие данные, полученные до того, как список был отсортирован. Атомные операции упрощают нашу жизнь, но есть сюрпризы. Например, + = кажется проще, чем sort (), но + = не атомарен. Как узнать, какие операции атомарны, а какие нет?

Рассмотрим этот код:

Мы исследуем глобальную блокировку интерпретатора Python и узнаем, как она влияет на многопоточные программы....-7

Мы можем увидеть байт-код, в который компилируется эта функция, с помощью стандартного Python-модуля:
Мы исследуем глобальную блокировку интерпретатора Python и узнаем, как она влияет на многопоточные программы....-8

Итак

 Обычно этот код печатает 100, потому что каждый из 100 потоков увеличил n.  Но иногда вы видите 99 или 98, если обновления одного из потоков были перезаписаны другим.
Обычно этот код печатает 100, потому что каждый из 100 потоков увеличил n. Но иногда вы видите 99 или 98, если обновления одного из потоков были перезаписаны другим.

Несмотря на GIL, вам все равно нужны блокировки для защиты общего изменяемого состояния:

Мы исследуем глобальную блокировку интерпретатора Python и узнаем, как она влияет на многопоточные программы....-10

Хотя GIL не освобождает нас от необходимости блокировок, это означает, что нет необходимости в мелкомасштабной блокировке. В языке со свободными потоками, как Java, программисты стараются заблокировать общие данные на как можно более короткое время, чтобы уменьшить конкуренцию потоков и обеспечить максимальный параллелизм. Однако, поскольку потоки не могут запускать Python параллельно, мелкозернистая блокировка не дает никаких преимуществ. Пока ни один поток не удерживает блокировку, пока он спит, не выполняет операции ввода-вывода или какую-либо другую операцию удаления GIL, вам следует использовать самые грубые и простые возможные блокировки.

Параллелизм

Что, если ваша задача завершится раньше, только если одновременно запустить код Python? Такой вид масштабирования называется параллелизмом, и GIL запрещает его. Вы должны использовать несколько процессов, что может быть сложнее, чем многопоточность, и требует больше памяти, но при этом будет использоваться несколько процессоров.

Этот пример завершается раньше, создавая 10 процессов, чем только один, потому что процессы выполняются параллельно на нескольких ядрах. Но он не будет работать быстрее с 10 потоками, чем с одним, потому что только один поток может выполнять Python одновременно:

Поскольку каждый разветвленный процесс имеет отдельный GIL, эта программа может разделить работу и выполнить несколько вычислений одновременно.
Поскольку каждый разветвленный процесс имеет отдельный GIL, эта программа может разделить работу и выполнить несколько вычислений одновременно.

Используйте потоки для одновременного ввода-вывода и процессы для параллельных вычислений. Принцип достаточно прост, и вам даже не придется писать его на руке.

Ставьте палец вверх чтобы видеть в своей ленте больше статей!
Подписывайтесь на мой канал здесь, а также на мой канал в телеграм, и вконтакте. Там вы можете почитать большое количество интересных материалов, а также задать свой вопрос.
Хотите задонатить в пользу канала?
Будем рады
5599005078807943 - mastercard
410018832650246 - ЮMoney