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

StackOverflow https://stackoverflow.com/questions/105095

  •  01-07-2019
  •  | 
  •  

Вопрос

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

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

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

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

Это было полезно?

Решение

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

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

Другие советы

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

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

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

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

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

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

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

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

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

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

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

и

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

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

Подумайте об этом с такой точки зрения:

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

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

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

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

b = 5

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

Таким образом, вы можете видеть, что даже если они на САМОМ деле не запущены одновременно, вам все равно нужна блокировка.

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

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

Любая операция / инструкция выполняется в интерпретаторе.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.
  • Таким образом, блокировка должна использоваться для сохранения как чтения, так и увеличения как атомарной операции.
  • Ответ Уилла Харриса объясняет это на примере кода.

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top