Являются ли блокировки ненужными в многопоточном коде Python из-за GIL?

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

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

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

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

Ответ 1

Вам все равно понадобятся блокировки, если вы разделяете состояние между потоками. 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), что приведет к утере обновления. Результатом является случайное значение для общего состояния.

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

Ответ 2

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

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

Ответ 3

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

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

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

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

Ответ 4

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

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

Ответ 5

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

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

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

и

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

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

Ответ 6

Подумайте об этом так:

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

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

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

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

b = 5

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

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

Ответ 7

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

Ответ 8

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

В интерпретаторе выполняется любая операция/инструкция. 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, а var становится 1.
  • GIL присваивается t2.
  • t2 думает read_var = 0, потому что это то, что он читал.
  • t2 выполняет var = read_var+1, а var становится равным 1.
  • Наше ожидание состояло в том, что var должно стать 2.
  • Таким образом, блокировка должна использоваться для сохранения и чтения и увеличения в качестве атомной операции.
  • Ответ Харриса объясняет это с помощью примера кода.

Ответ 9

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

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 действительно разрешает только один поток в любое время (временная разбивка потоков), тогда не должно существовать существование устаревшего значения, правильно?