Что такое глобальная блокировка интерпретатора Python (GIL)?

Что такое глобальная блокировка интерпретатора Python (GIL)?

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

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

Поскольку GIL позволяет одновременно выполнять только один поток, даже в многопоточной архитектуре с более чем одним ядром процессора, GIL приобрел репутацию «печально известной» функции Python.

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

Какую проблему решил GIL для Python?

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

Давайте рассмотрим краткий пример кода, чтобы продемонстрировать, как работает подсчет ссылок:

>>>

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

В приведенном выше примере счетчик ссылок для объекта пустого списка[] был равен 3. На объект списка ссылалисьa,b, и аргумент был переданsys.getrefcount().

Вернуться к GIL:

Проблема заключалась в том, что эта переменная подсчета ссылок нуждалась в защите от условий гонки, когда два потока увеличивают или уменьшают свое значение одновременно. Если это произойдет, это может привести к утечке памяти, которая никогда не будет освобождена, или, что еще хуже, к неправильному освобождению памяти, пока существует ссылка на этот объект. Это может вызвать сбои или другие «странные» ошибки в ваших программах на Python.

Эту переменную счетчика ссылок можно сохранить в безопасности, добавивlocks ко всем структурам данных, которые совместно используются потоками, чтобы они не изменялись непоследовательно.

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

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

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

С другой стороны, это означает, что эти языки часто должны компенсировать потерю однопоточных преимуществ производительности GIL, добавляя другие функции повышения производительности, такие как JIT-компиляторы.

Почему GIL был выбран в качестве решения?

Итак, почему подход, который, по-видимому, является настолько препятствующим, использовался в Python? Было ли это плохим решением разработчиков Python?

Что ж, вwords of Larry Hastings дизайнерское решение GIL - одна из тех вещей, которые сделали Python таким же популярным, как и сегодня.

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

Было написано много расширений для существующих библиотек C, функции которых были необходимы в Python. Чтобы предотвратить несогласованные изменения, этим расширениям C требовалось поточно-ориентированное управление памятью, предоставляемое GIL.

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

Библиотеки C, которые не были поточно-ориентированными, стали легче интегрировать. И эти расширения C стали одной из причин, почему Python был легко принят различными сообществами.

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

Влияние на многопоточные программы Python

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

Программы с привязкой к ЦП - это программы, которые нагружают ЦП до предела. Это включает в себя программы, которые выполняют математические вычисления, такие как умножение матриц, поиск, обработка изображений и т. Д.

I/O-bound programs are the ones that spend time waiting for Input/Output which can come from a user, file, database, network, etc. I/O-bound programs sometimes have to wait for a significant amount of time till they get what they need from the source due to the fact that the source may need to do its own processing before the input/output is ready, for example, a user thinking about what to enter into an input prompt or a database query running in its own process.

Давайте посмотрим на простую программу с привязкой к процессору, которая выполняет обратный отсчет:

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)

Запуск этого кода в моей системе с 4 ядрами дал следующий результат:

$ python single_threaded.py
Time taken in seconds - 6.20024037361145

Теперь я немного изменил код, чтобы сделать тот же отсчет, используя два потока параллельно:

# multi_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time taken in seconds -', end - start)

И когда я снова запустил это:

$ python multi_threaded.py
Time taken in seconds - 6.924342632293701

Как видите, обе версии требуют почти одинакового времени для завершения. В многопоточной версии GIL препятствовал выполнению связанных с процессором потоков в parellel.

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

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

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

Почему GIL еще не был удален?

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

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

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

Создатель и BDFL Python, Гвидо ван Россум, дал ответ сообществу в сентябре 2007 года в своей статье“It isn’t Easy to remove the GIL”:

«Я приветствовал бы набор исправлений в Py3konly if производительности для однопоточной программы (и для многопоточной, но связанной с вводом-выводом программы)does not decrease»

И это условие не было выполнено ни одной из попыток, предпринятых с тех пор.

Почему он не был удален в Python 3?

Python 3 действительно имел шанс запустить множество функций с нуля и в процессе работы сломал некоторые из существующих расширений C, которые затем потребовали обновления и перенести изменения для работы с Python 3. По этой причине ранние версии Python 3 стали медленнее восприниматься сообществом.

Но почему GIL не был удален вместе?

Удаление GIL сделало бы Python 3 более медленным по сравнению с Python 2 в однопоточном исполнении, и вы можете представить, к чему это привело бы. Вы не можете спорить с однопоточными преимуществами производительности GIL. В результате у Python 3 все еще есть GIL.

Но Python 3 внес существенное улучшение в существующий GIL -

Мы обсуждали влияние GIL на многопоточные программы «только с привязкой к процессору» и «только с привязкой к вводу / выводу», но как быть с программами, в которых некоторые потоки связаны с вводом / выводом, а другие - с процессором?

В таких программах GIL Python, как известно, истощал потоки, связанные с вводом / выводом, не давая им возможности получить GIL из потоков, связанных с процессором.

Это произошло из-за механизма, встроенного в Python, который заставлял потоки освобождать GILafter a fixed intervalот непрерывного использования, и если никто другой не получил GIL, тот же поток мог продолжить его использование.

>>>

>>> import sys
>>> # The interval is set to 100 instructions:
>>> sys.getcheckinterval()
100

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

Эта проблема была исправлена ​​в Python 3.2 в 2009 году Антуаном Питру, которыйadded a mechanism смотрел на количество запросов на получение GIL другими потоками, которые были сброшены, и не позволял текущему потоку повторно получить GIL до того, как другие потоки получили возможность запустить .

Как бороться с GIL Python

Если GIL вызывает у вас проблемы, вот несколько подходов, которые вы можете попробовать:

Multi-processing vs multi-threading: Самый популярный способ - использовать многопроцессорный подход, когда вы используете несколько процессов вместо потоков. Каждый процесс Python получает свой собственный интерпретатор Python и пространство памяти, поэтому GIL не будет проблемой. В Python есть модульmultiprocessing, который позволяет нам легко создавать такие процессы:

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n>0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds -', end - start)

Запуск этого в моей системе дал такой вывод:

$ python multiprocess.py
Time taken in seconds - 4.060242414474487

Достойный прирост производительности по сравнению с многопоточной версией, верно?

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

Alternative Python interpreters: Python имеет несколько реализаций интерпретатора. CPython, Jython, IronPython и PyPy, написанные на C, Java, C # и Python соответственно, являются наиболее популярными. GIL существует только в оригинальной реализации Python, которая называется CPython. Если ваша программа с ее библиотеками доступна для одной из других реализаций, то вы также можете попробовать их.

Just wait it out: Хотя многие пользователи Python пользуются преимуществами однопоточной производительности GIL. Многопоточным программистам не нужно беспокоиться, так как некоторые из самых ярких умов в сообществе Python работают над удалением GIL из CPython. Одна из таких попыток известна какGilectomy.

Python GIL часто рассматривается как загадочная и сложная тема. Но имейте в виду, что как Pythonista на вас обычно влияет только то, что вы пишете расширения C или используете многопоточность с привязкой к ЦП в своих программах.

В этом случае эта статья должна дать вам все, что вам нужно, чтобы понять, что такое GIL и как с ним обращаться в ваших собственных проектах. И если вы хотите понять низкоуровневую внутреннюю работу GIL, я бы рекомендовал вам посмотреть доклад Дэвида БизлиUnderstanding the Python GIL.