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