Не нужны ли блокировки в многопоточном коде Python из-за GIL?
Если вы полагаетесь на реализацию Python, которая имеет глобальную блокировку интерпретатора (т.е. CPython), и пишете многопоточный код, вам действительно нужны блокировки вообще?
Если GIL не позволяет выполнять несколько инструкций параллельно, не будет ли необходимости защищать общие данные?
извините, если это глупый вопрос, но я всегда задавался вопросом о Python на многопроцессорных / ядерных машинах.
То же самое относится и к любой другой языковой реализации, которая имеет GIL.
9 ответов
Вам по-прежнему понадобятся блокировки, если вы разделяете состояние между потоками. 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
), вызывая потерянное обновление. Результатом является случайное значение для общего состояния.
Чтобы обновления были согласованными, методы run должны были бы заблокировать общее состояние вокруг разделов чтения-изменения-записи (внутри циклов) или иметь какой-либо способ определить, когда общее состояние изменилось с момента его чтения.
Нет - GIL просто защищает внутренние компоненты Python от множества потоков, изменяющих их состояние. Это очень низкий уровень блокировки, достаточный только для поддержания собственных структур python в согласованном состоянии. Он не распространяется на блокировку уровня приложения, которую вам нужно сделать, чтобы обеспечить безопасность потоков в вашем собственном коде.
Суть блокировки заключается в том, чтобы гарантировать, что определенный блок кода выполняется только одним потоком. GIL применяет это для блоков размером с один байт-код, но обычно вы хотите, чтобы блокировка охватывала больший блок кода, чем этот.
Добавление к обсуждению:
Поскольку GIL существует, некоторые операции в Python являются атомарными и не нуждаются в блокировке.
http://www.python.org/doc/faq/library/
Однако, как указано в других ответах, вам все равно необходимо использовать блокировки всякий раз, когда их требует логика приложения (например, в проблеме производителя / потребителя).
Этот пост описывает GIL на довольно высоком уровне:
Особый интерес представляют эти цитаты:
Каждые десять инструкций (это значение по умолчанию можно изменить), ядро освобождает GIL для текущего потока. В этот момент ОС выбирает поток из всех потоков, конкурирующих за блокировку (возможно, выбирая тот же поток, который только что выпустил GIL - у вас нет никакого контроля над тем, какой поток выбирается); этот поток получает GIL, а затем работает еще десять байт-кодов.
а также
Обратите внимание, что GIL ограничивает только чистый код Python. Могут быть написаны расширения (внешние библиотеки Python, обычно написанные на C), которые снимают блокировку, которая затем позволяет интерпретатору Python работать отдельно от расширения, пока расширение не повторно захватит блокировку.
Похоже, что GIL просто предоставляет меньше возможных экземпляров для переключения контекста и заставляет многоядерные / процессорные системы вести себя как одно ядро по отношению к каждому экземпляру интерпретатора Python, так что да, вам все равно нужно использовать механизмы синхронизации.
Глобальная блокировка интерпретатора предотвращает одновременный доступ потоков к интерпретатору (таким образом, CPython всегда использует только одно ядро). Однако, насколько я понимаю, потоки все еще прерываются и планируются с преимуществом, что означает, что вам все еще нужны блокировки на общих структурах данных, чтобы ваши потоки не топали друг друга.
Ответ, с которым я сталкиваюсь снова и снова, заключается в том, что многопоточность в Python редко стоит накладных расходов из-за этого. Я слышал хорошие вещи о проекте PyProcessing, который делает запуск нескольких процессов "простым", как многопоточность, с общими структурами данных, очередями и т. Д. (PyProcessing будет введен в стандартную библиотеку будущего Python 2.6 в качестве модуля многопроцессорной обработки)..) Это поможет вам справиться с GIL, поскольку у каждого процесса есть свой интерпретатор.
Думайте об этом так:
На однопроцессорном компьютере многопоточность происходит путем приостановки одного потока и запуска другого достаточно быстро, чтобы казалось, что он работает одновременно. Это похоже на Python с GIL: на самом деле работает только один поток.
Проблема в том, что поток может быть приостановлен в любом месте, например, если я хочу вычислить b = (a + b) * 3, это может привести к инструкциям примерно так:
1 a += b
2 a *= 3
3 b = a
Теперь предположим, что он выполняется в потоке, и этот поток приостанавливается после строки 1 или 2, а затем запускается и запускается другой поток:
b = 5
Затем, когда другой поток возобновляет работу, b перезаписывается старыми вычисленными значениями, что, вероятно, не то, что ожидалось.
Таким образом, вы можете видеть, что, хотя они не работают одновременно, вам все равно нужно заблокировать.
Замки все еще нужны. Я постараюсь объяснить, зачем они нужны.
Любая операция / инструкция выполняется в интерпретаторе. GIL гарантирует, что переводчик удерживается одним потоком в определенный момент времени. И ваша программа с несколькими потоками работает в одном интерпретаторе. В любой конкретный момент времени этот переводчик удерживается одним потоком. Это означает, что в любой момент времени работает только поток, в котором находится интерпретатор.
Предположим, есть два потока, скажем, t1 и t2, и оба хотят выполнить две инструкции, которые считывают значение глобальной переменной и увеличивают его.
#increment value
global var
read_var = var
var = read_var + 1
Как указано выше, GIL только гарантирует, что два потока не могут выполнить инструкцию одновременно, что означает, что оба потока не могут выполнить read_var = var
в любой конкретный момент времени. Но они могут выполнять инструкции один за другим, и у вас все еще могут быть проблемы. Рассмотрим эту ситуацию:
- Предположим, что read_var равен 0.
- GIL удерживается потоком t1.
- Т1 выполняет
read_var = var
, Таким образом, read_var в t1 равен 0. GIL гарантирует, что эта операция чтения не будет выполнена ни для какого другого потока в данный момент. - GIL дается потоку t2.
- Т2 выполняет
read_var = var
, Но read_var по-прежнему равен 0. Итак, read_var в t2 равен 0. - GIL дается на t1.
- Т1 выполняет
var = read_var+1
и вар становится 1. - GIL дается в t2.
- t2 думает, что read_var=0, потому что это то, что он прочитал.
- Т2 выполняет
var = read_var+1
и вар становится 1. - Мы ожидали, что
var
должно стать 2. - Таким образом, блокировка должна использоваться для сохранения чтения и приращения как атомарной операции.
- Ответ Уилла Харриса объясняет это на примере кода.
Вам все еще нужно использовать блокировки (ваш код может быть прерван в любое время для выполнения другого потока, что может привести к несоответствиям данных). Проблема с GIL заключается в том, что он не позволяет коду Python использовать больше ядер одновременно (или несколько процессоров, если они доступны).
Немного обновления из примера Уилла Харриса:
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)
Если я правильно понимаю, вышеуказанная проверка условий не будет работать в реальной среде потоков. Когда одновременно выполняется более одного потока, может быть создано устаревшее значение, что приводит к несогласованности состояния общего ресурса, тогда вам действительно нужна блокировка. Но если python действительно допускает только один поток в любое время (разделение на потоки по времени), то не должно быть возможности существования устаревших значений, верно?