python - управления - примеры многопоточных приложений




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

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

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

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


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

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

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

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


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

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


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

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

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

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

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

b = 5

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

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





locking