GIL (Global Interpreter Lock) в Python


05.03.2020 — Про Python


Изначально я хотел сразу начать отвечать на вопрос с собеса про память в Python, но поняв, что выйдет очень длинно, решил разбить на несколько постов.

Сейчас поговорим про GIL, в следующих статьях про GC, закончим уже поверхностно про пуллы памяти, арены.

GIL - это просто лок, который разрешает только одному потоку удерживать контроль на интерпретатором Python'a (собсна такое определение можно было дать и из названия).

Не просто так конечно выделил жирным про один поток. Да, в этом вся печаль и рофел. Никакого параллельного выполнения потоков у нас НЕТ! Когда речь идет о однопоточной программе, то вообще всё равно, но если мы работаем в нескольких потоках, то это бьёт нам по производительности. Всё это можно сравнить с ядрами нашего ЦП. Мы всегда исключительно используем одно, а другие просто наблюдают как другой напрягается.

GIL нужен для решения проблемы race conditions. Сразу на примере, но придется затронуть чутка сборщик мусора. У каждого объекта (PyObject) есть Py_REFCNT где хранится количество ссылок на этот объект. Когда количество ссылок становится равным нулю - происходит освобождение памяти. Если количество ссылок == 0, то, понятное дело, объект нигде не используется и достучаться до него из кода уже невозможно. Возвращаемся к проблеме. Когда у нас многопоточное приложение, то один поток может удалить ссылку и сделать -= 1 к данной переменной, а другое наоборот захочет сделать += 1. Желание у них появилось одновременно и непонятно какое значение у переменной будет в конце. В итоге это приведет к непоняткам. Или объект случайно удалится GC, хотя на него есть ссылка где-то, или наоборот, никогда не удалится. Будет дичь.

Решением это дичи будет добавление локов к каждому объекту, что использует эту переменную с количеством ссылок. Дабы организовать последовательное обращение к переменной. Стоит лок - ждем. Свободно - заходим и лочимся за собой. Когда уходим - разлокиваем. Звучит просто, но лок в каждом объекте приводит во-первых к их огромному количеству, а во-вторых к дедлокам. Дедлок - это такая ситуация, что мы сидим, ждем пока там освободят эту переменку, но она не освобождается. Почему? Потому что кто её ждет сам её и удерживает.

Поэтому GIL -> Global -> single и так мы пришли да, к одному общему, глобальному локу для всех.

Данный лок элементарно и круто справляется со своей задачей. У нас никого не возникает проблем. Никаких гонок, никаких дедлоков, ляпота же. Только вот мы теряем производительность. Сам этот GIL был написан чертовски давно. То, что используется сейчас, было написано ещё 10 лет назад (в 2009) и особых изменений с того времени не было.

GIL был выбран ещё на этапах проектирования Python'a. Как обычно в моих статьях, не могу не сказать про то, что Python является простым и для всех, для любых задач. Собсна тогда (да и сейчас) многие расширения на C требовали безопасного менеджмента памяти, а GIL это давал с помощью очень простого решения. В общем, так исторически сложилось и сейчас он так сильно врос, что от него сложно избавиться, но процесс уже запущен (сабинтепретаторы в Python 3.8). Нельзя просто так взять и выбросить GIL (можно, это уже делали), ибо это сломает огромное количество всё тех же расширений C.

Мы получаем в CPython какой-то флаг (или семафор) и каждый поток должен запрашивать у нашего GIL'a доступ. Сама блокировка находится в основном цикле нашего байткода, о байткоде я писал в одной из прошлой статье. В текущей реализации нет приоритетов, нет четкой последовательности какой поток получит следующим доступ. Ничего этого нет. Всё это перекладывается на ОС.

Вверху всё было про то, что уже есть. Однако в последних версиях Python'a, как я уже упомянул чуть выше, есть subinterpreters. Суть проста: у нас есть процесс, у него есть несколько интерпретаторов (о то что это тоже в прошлой статье), у каждого интерпретатора конечно же свой GIL, а уже у интерпретаторов свой набор потоков. Конечно нам нужен доступ какой-то общий между двумя интерпретаторами, но у нас ведь два GIL'a. Поэтому предлагается добавить общую память между ними и использовать указатели для поиска объектов. В данном случае управлять блокировками между интерпретаторами будет сам процесс. Но как я понял, какого-то API для такого шардирования ещё нет.

PEP 554 как раз про сабинтерпретаторы, шардирование и бла-бла (там вон даже файловые дескрипторы шарят)

Ниже классный старый видосик для куда более тонкого понимания GIL'a, но мне хватит и текущих знаний (пока что), а Вам?

https://www.youtube.com/watch?v=Obt-vMVdM8s

Недавние посты


© marshal.by 2020

Исходный код

Сайт работает на Gatsby + prismic и опубликован на GitHub.