Не нужны ли блокировки в многопоточном коде Python из-за GIL? - PullRequest
67 голосов
/ 20 сентября 2008

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

Если GIL не допускает параллельного выполнения нескольких инструкций, не нужно ли защищать совместно используемые данные?

извините, если это глупый вопрос, но я всегда задавался вопросом о Python на многопроцессорных / ядерных машинах.

То же самое применимо к любой другой языковой реализации с GIL.

Ответы [ 9 ]

68 голосов
/ 20 сентября 2008

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

Например:

#!/usr/bin/env python
import threading

shared_balance = 0

class Deposit(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance += 100
            shared_balance = balance

class Withdraw(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance -= 100
            shared_balance = balance

threads = [Deposit(), Withdraw()]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print shared_balance

Здесь ваш код может быть прерван между чтением общего состояния (balance = shared_balance) и записью измененного результата обратно (shared_balance = balance), что приведет к потере обновления. Результатом является случайное значение для общего состояния.

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

22 голосов
/ 20 сентября 2008

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

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

10 голосов
/ 28 октября 2008

Добавление к обсуждению:

Поскольку GIL существует, некоторые операции в Python являются атомарными и не нуждаются в блокировке.

http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe

Однако, как указано в других ответах, вам все же необходимо использовать блокировки, когда этого требует логика приложения (например, в проблеме производителя / потребителя).

8 голосов
/ 20 сентября 2008

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

Ответ, с которым я сталкивался снова и снова, заключается в том, что многопоточность в Python редко стоит накладных расходов из-за этого. Я слышал хорошие вещи о проекте PyProcessing , который делает запуск нескольких процессов «простым», как многопоточность, с общими структурами данных, очередями и т. Д. (PyProcessing будет введен в стандартную библиотеку будущего Python 2.6 как многопроцессорный модуль.) Это поможет вам обойти GIL, поскольку у каждого процесса есть свой интерпретатор.

8 голосов
/ 20 сентября 2008

Этот пост описывает GIL на довольно высоком уровне:

Особый интерес представляют эти цитаты:

Каждые десять инструкций (по умолчанию можно изменить), ядро ​​освобождает GIL для текущей темы. При этом точка, ОС выбирает поток из все нити, конкурирующие за блокировку (возможно, выбрав ту же тему который только что выпустил GIL - вы не иметь какой-либо контроль над тем, какой поток выбирается); эта нить приобретает GIL, а затем работает еще на десять байткоды.

и

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

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

3 голосов
/ 22 декабря 2008

Думайте об этом так:

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

Проблема в том, что поток может быть приостановлен где угодно, например, если я хочу вычислить b = (a + b) * 3, это может привести к инструкциям примерно так:

1    a += b
2    a *= 3
3    b = a

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

b = 5

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

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

1 голос
/ 05 декабря 2013

Замки все еще нужны. Я постараюсь объяснить, зачем они нужны.

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

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

#increment value
global var
read_var = var
var = read_var + 1

Как указано выше, GIL гарантирует, что два потока не могут выполнить инструкцию одновременно, что означает, что оба потока не могут выполнить read_var = var в любой конкретный момент времени. Но они могут выполнять инструкции один за другим, и у вас все еще могут быть проблемы. Рассмотрим следующую ситуацию:

  • Предположим, read_var равен 0.
  • GIL удерживается потоком t1.
  • t1 выполняет read_var = var. Итак, read_var в t1 равен 0. GIL гарантирует, что эта операция чтения не будет выполнена ни для какого другого потока в данный момент.
  • GIL дается потоку t2.
  • t2 выполняет read_var = var. Но read_var по-прежнему равен 0. Итак, read_var в t2 равен 0.
  • GIL дается в t1.
  • t1 выполняет var = read_var+1, а переменная становится 1.
  • GIL дается в t2.
  • t2 считает read_var = 0, потому что это то, что он прочитал.
  • t2 выполняет var = read_var+1 и var становится 1.
  • Мы ожидали, что var должно стать 2.
  • Таким образом, блокировка должна использоваться для сохранения и чтения, и приращения как атомарной операции.
  • Ответ Уилла Харриса объясняет это на примере кода.
1 голос
/ 20 сентября 2008

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

0 голосов
/ 22 декабря 2008

Небольшое обновление из примера Уилла Харриса:

class Withdraw(threading.Thread):  
def run(self):            
    for _ in xrange(1000000):  
        global shared_balance  
        if shared_balance >= 100:
          balance = shared_balance
          balance -= 100  
          shared_balance = balance

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

Если GIL предотвращает выполнение только одного потока в любое атомное время, то где будет устаревшее значение? Если нет устаревшего значения, зачем нам блокировка? (Предполагается, что мы говорим только о чистом коде Python)

Если я правильно понимаю, вышеупомянутая проверка условий не будет работать в поточной среде real . Когда одновременно выполняется более одного потока, может быть создано устаревшее значение, что приводит к несогласованности состояния общего ресурса, тогда вам действительно нужна блокировка. Но если python действительно допускает только один поток в любое время (разделение на потоки по времени), тогда не должно быть возможности для устаревшего значения, верно?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...