При работе с потоками, а также при обсуждении быстродействия в Python, часто можно услышать про проблему GIL (Global Interpreter Lock). Если кратко, то GIL позволяет работать только одному потоку внутри процесса. Это приводит к снижению производительности многопоточных программ.
При работе нескольких потоков в Python возможны конфликты при обращении к одним и тем же участкам памяти, поэтому здесь нужна глобальная блокировка для корректного управления. Python во время работы подсчитывает количество ссылок на объекты для корректной работы сборщика мусора. Всё в Python является объектами, и каждый объект имеет атрибут refcount, в котором хранится число ссылок на этот объект. Как только он становится равным нулю, объект удаляется из памяти.
Подсчёт ссылок на объект
Рассмотрим небольшой пример с подсчётом ссылок на объект.
import sys
s = "подсчет количества ссылок на объект"
print(sys.getrefcount(s))
s2 = s
print(sys.getrefcount(s))В результате в консоль выведется:
2
3При вызове функции getrefcount и передаче в неё переменной s аргумент функции также будет ссылаться на объект. Поэтому изначально, после создания вызова функции, ссылок две. Затем, после присваивания переменной s2, ссылок становится три.
Какую проблему решает GIL
Одна из проблем, которую решает GIL, следующая: несколько потоков в многопоточном приложении могут одновременно изменять значение счётчика ссылок у переменной. Это может привести к тому, что в какой-то момент число ссылок на объект станет равным нулю и он удалится из памяти. Но при этом далее в процессе выполнения он ещё должен был использоваться. В результате код будет работать некорректно.
Счётчик ссылок можно защитить, добавив блокировки на все структуры данных, которые распространяются по нескольким потокам. В таком случае счётчик будет изменяться последовательно. Но добавление блокировки к нескольким объектам может привести к появлению другой проблемы — взаимоблокировки (deadlock). Она случается, если блокировка стоит более чем на одном объекте. К тому же это тоже снижало бы производительность из-за многократной установки блокировки.
В итоге GIL превращает любую многопоточную программу в однопоточную.
Когда потоки всё же полезны
Но стоит ли вообще работать с потоками в Python с учётом всех этих ограничений? Ответ — да. Потоки можно использовать, если распараллеливаемый код занят в основном операциями ввода-вывода (IO-bound operations): запросами в БД, чтением или записью файлов, запросами к внешним URL. В этом случае переменные, а значит и счётчик ссылок, изменяться не будут, а GIL просто передаст управление другому потоку, пока первый ожидает завершения операции ввода-вывода.
Наиболее простой способ обойти GIL — использование нескольких процессов вместо потоков. Но в этом случае необходимо учитывать, что у процессов нет общей памяти и нужно дополнительно позаботиться о передаче данных между процессами.