GIL убит?

Python
GIL убит?

Эта статья является оригинальной и впервые опубликована в публичном аккаунте【Питон кот], пожалуйста, не перепечатывайте без разрешения.

Оригинальный адрес:Tickets.WeChat.QQ.com/Yes/8kV QE mz0S…

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

После того, как был предложен PEP-554 (сентябрь 2017 г.), все, кажется, видят проблеск улучшения. Однако действительно ли GIL можно полностью убить, и если да, то как он будет реализован, и почему его не внедряют уже больше года, сколько нам еще ждать?


английский | Has the Python GIL been slain?【1】

автор | Anthony Shaw

переводчик| Кошка под цветком гороха

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

В начале 2003 года Intel представила новый процессор Pentium 4 «HT» с тактовой частотой 3 ГГц и технологией «Hyper-Threading».

В течение следующих нескольких лет Intel и AMD жестко конкурировали за достижение максимально возможной производительности настольных компьютеров за счет увеличения скорости шины, размера кэш-памяти L2 и уменьшения размера микросхемы для минимизации задержки. 3Ghz HT был заменен в 2004 году моделью Prescott 580 с тактовой частотой до 4 ГГц.

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

У вас есть процессор 4Ghz на вашем компьютере? Маловероятно, поскольку путь к повышению производительности — это более высокая скорость шины и большее количество ядер. Intel Core 2 Duo, пришедший на смену Pentium 4 в 2006 году, имеет тактовую частоту значительно ниже этой.

В дополнение к выпуску многоядерных процессоров потребительского уровня в 2006 году произошли и другие события, был выпущен Python 2.5! Python 2.5 содержит бета-версию любимого оператора with.

Python 2.5 имеет важное ограничение при использовании Intel Core 2 Duo или AMD Athlon X2.GIL.

Что такое ГИЛ?

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

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

GIL упрощает многопоточное программирование на Python.

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

Текущая версия GILбыл написан в 2009 году[2], используемый для поддержки асинхронной функциональности, остался почти нетронутым, даже после многочисленных попыток удалить его или уменьшить зависимости от него.

Привлекательность всех предложений по удалению GIL заключается в том, что он не должен снижать производительность однопоточного кода. Любой, кто включил Hyper-Threading в 2003 году, поймет, почемуэто важно【3】.

Избегайте GIL в CPython

Если вы хотите использовать действительно параллельный код в CPython, вы должны использовать многопроцессорность.

В CPython 2.6 стандартная библиотека добавилаmultiprocessingмодуль. многопроцессорность - это оболочка вокруг порожденных процессов CPython (каждый со своим собственным GIL) -

from multiprocessing import Process

def f(name):
    print 'hello', name

if __name__ == '__main__':
    p = Process(target=f, args=('bob',))
    p.start()
    p.join()

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

multiprocessingМодули также поддерживают совместное использование переменных через очереди или каналы. У него есть объект Lock, который блокирует объекты в основном процессе, чтобы другие процессы могли писать.

Многопроцессорная обработка имеет один существенный недостаток: она требует больших затрат времени и памяти. Время запуска CPython даже без no-site составляет 100-200 мс (см.эта ссылка[4]).

Таким образом, вы можете использовать параллельный код в CPython, но вы должны тщательно планировать те длительные процессы, которые редко обмениваются объектами между собой.

Другой альтернативой является использование сторонней библиотеки, такой как Twisted.

PEP-554 и смерть ГИЛ?

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

Есть ли лучшее решение?

Ключ к обходу GIL содержится в его названии, глобальномустный переводчикБлокировки являются частью глобального состояния интерпретатора. Процесс CPython может иметь несколько интерпретаторов и, следовательно, несколько блокировок, но эта функция редко используется, поскольку она доступна только через C-API.

Среди функций, предлагаемых для CPython 3.8, есть PEP-554, в котором предлагается реализовать субинтерпретатор и предоставить новый API с API в стандартной библиотеке.interpretersмодуль.

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

Поскольку состояние интерпретатора содержит область выделения памяти, набор всех указателей на объекты Python (локальные и глобальные), подинтерпретаторы в PEP-554 не могут получить доступ к глобальным переменным других интерпретаторов.

Как и в случае с многопроцессорной обработкой, способ совместного использования объектов между интерпретаторами заключается в использовании некоторой формы IPC (сети, диска или общей памяти) для сериализации. В Python существует множество способов сериализации объектов, например.marshalмодуль,pickleмодули иjsonиsimplexmlЭто более стандартизированный подход. Эти методы смешаны, но всегда влекут за собой дополнительные накладные расходы.

Лучшее решение — открыть разделяемое переменное пространство памяти, контролируемое основным процессом. Таким образом, объекты могут быть отправлены из основного интерпретатора и получены другими интерпретаторами. Это будет управляемое памятью пространство для указателей PyObject, доступ к которому может получить каждый интерпретатор, в то время как основной процесс контролирует блокировку.

Такой API еще прорабатывается, но он может выглядеть так:

import _xxsubinterpreters as interpreters
import threading
import textwrap as tw
import marshal

# Create a sub-interpreter
interpid = interpreters.create()

# If you had a function that generated some data
arry = list(range(0,100))

# Create a channel
channel_id = interpreters.channel_create()

# Pre-populate the interpreter with a module
interpreters.run_string(interpid, "import marshal; import _xxsubinterpreters as interpreters")

# Define a
def run(interpid, channel_id):
    interpreters.run_string(interpid,
                            tw.dedent("""
        arry_raw = interpreters.channel_recv(channel_id)
        arry = marshal.loads(arry_raw)
        result = [1,2,3,4,5] # where you would do some calculating
        result_raw = marshal.dumps(result)
        interpreters.channel_send(channel_id, result_raw)
        """),
               shared=dict(
                   channel_id=channel_id
               ),
               )

inp = marshal.dumps(arry)
interpreters.channel_send(channel_id, inp)

# Run inside a thread
t = threading.Thread(target=run, args=(interpid, channel_id))
t.start()

# Sub interpreter will process. Feel free to do anything else now.
output = interpreters.channel_recv(channel_id)
interpreters.channel_release(channel_id)
output_arry = marshal.loads(output)

print(output_arry)

В этом примере используется numpy и он отправляет массив numpy по каналу путем его сериализации с помощью модуля marshal, а затем данные обрабатываются вспомогательным интерпретатором (на отдельном GIL), поэтому это требует больших вычислительных ресурсов (с привязкой к процессору). проблемы параллелизма, подходящие для обработки с субинтерпретаторами.

Это выглядит неэффективно

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

PEP-574 предлагает новый рассол[5] Протокол (v5), поддерживающий обработку буферов памяти отдельно от остального потока рассола. Для больших объектов данных однократная сериализация и десериализация субинтерпретатором добавляет много накладных расходов.

Новый API может (гипотетически, не объединенный) предоставить такой интерфейс:

import _xxsubinterpreters as interpreters
import threading
import textwrap as tw
import pickle

# Create a sub-interpreter
interpid = interpreters.create()

# If you had a function that generated a numpy array
arry = [5,4,3,2,1]

# Create a channel
channel_id = interpreters.channel_create()

# Pre-populate the interpreter with a module
interpreters.run_string(interpid, "import pickle; import _xxsubinterpreters as interpreters")

buffers=[]

# Define a
def run(interpid, channel_id):
    interpreters.run_string(interpid,
                            tw.dedent("""
        arry_raw = interpreters.channel_recv(channel_id)
        arry = pickle.loads(arry_raw)
        print(f"Got: {arry}")
        result = arry[::-1]
        result_raw = pickle.dumps(result, protocol=5)
        interpreters.channel_send(channel_id, result_raw)
        """),
                            shared=dict(
                                channel_id=channel_id,
                            ),
                            )

input = pickle.dumps(arry, protocol=5, buffer_callback=buffers.append)
interpreters.channel_send(channel_id, input)

# Run inside a thread
t = threading.Thread(target=run, args=(interpid, channel_id))
t.start()

# Sub interpreter will process. Feel free to do anything else now.
output = interpreters.channel_recv(channel_id)
interpreters.channel_release(channel_id)
output_arry = pickle.loads(output)

print(f"Got back: {output_arry}")

Это выглядит как много шаблонов

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

Как только этот PEP будет включен, я думаю, что некоторые другие API в PyPi также примут его.

Сколько накладных расходов стоит субинтерпретатор?

Короткий ответ: более одного потока, менее одного процесса.

Подробный ответ: у интерпретатора есть собственное состояние, поэтому, хотя PEP-554 упрощает создание подинтерпретаторов, он также требует клонирования и инициализации следующего:

  • существуетmainПространства имен и модули в importlib
  • Содержимое системного словаря
  • Встроенные методы (print, assert и т. д.)
  • нить
  • основная конфигурация

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

А как насчет асинкио?

в стандартной библиотекеasyncioТекущая реализация цикла обработки событий создает кадры, которые необходимо оценивать, но разделяет состояние (и, следовательно, GIL) в основном интерпретаторе.

После слияния PEP-554, вероятно, в Python 3.9 появилась альтернативная реализация цикла событий.возможныйВот оно (хотя этого еще никто не делал): запускайте асинхронные методы внутри подинтерпретатора и, таким образом, будьте параллельны.

Звучит отлично, отправляйте!

Ну, еще нет.

Поскольку CPython уже давно использует реализацию с одним интерпретатором, «Состояние выполнения» используется вместо «Состояние интерпретатора» во многих местах кодовой базы, поэтому, если вы хотите использовать текущую версию, если включен PEP-554, это вызовет много проблем.

Например, состояние сборщика мусора (до версии 3.7) принадлежит среде выполнения.

существуетPyCon sprintВ течение периода разработчики добровольно присоединяются к проекту для выполнения «спринтерской» разработки. Это слово больше используется agile-командами разработчиков, и значение и форма будут немного отличаться),Изменение началось[6] Передать состояние сборщика мусора интерпретатору, чтобы у каждого подинтерпретатора был свой GC (как и должно быть).

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

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

Короче говоря, есть много других вещей, которые необходимо решить.

Вывод: GIL мертв?

Для однопоточных приложений GIL все еще жив. Таким образом, даже с включением PEP-554, если у вас есть однопоточный код, он не станет вдруг параллельным.

Если вы хотите использовать параллельный код в Python 3.8 и у вас есть проблемы с параллельным выполнением, требующие больших вычислительных ресурсов, это может быть билетом!

Когда?

Pickle v5 и разделяемая память для многопроцессорной обработки, скорее всего, будут реализованы в Python 3.8 (октябрь 2019 г.), субинтерпретатор будет между 3.8 и 3.9.

Если вы хотите использовать мой пример сейчас, я создал ветку со всеминеобходимый код【7】

References

[1] Has the Python GIL been slain? hacker noon.com/ha-shi-he-and-park yo-chun…[2] было написано в 2009 году:GitHub.com/Python/Использование продукта…[3] Это важно:Товары Арест Вы Rub.com/features/20…[4] Эта ссылка:hacker noon.com/what-is-diva…[5] PEP-574 предлагает новый рассол:Woohoo.Python.org/Dev/PEPs/PE…[6] Начались изменения:GitHub.com/Python/Использование продукта…[7] Необходимый код:GitHub.com/Тони вздор…

публика【Питон кот], в этом выпуске публикуется серия высококачественных статей, в том числе серия Meow Star Philosophy Cat, расширенная серия Python, серия рекомендаций по хорошим книгам, технические статьи, высококачественные рекомендации и перевод на английский язык и т. д. Добро пожаловать, обратите внимание. Закулисный ответ"люблю учиться” и получите бесплатный обучающий пакет.