Другие люди дали очень хорошие определения. Вот классический пример:
import threading
account_balance = 0 # The "resource" that zenazn mentions.
account_balance_lock = threading.Lock()
def change_account_balance(delta):
global account_balance
with account_balance_lock:
# Critical section is within this block.
account_balance += delta
Предположим, что оператор +=
состоит из трех подкомпонентов:
- Считать текущее значение
- Добавить RHS к этому значению
- Записать накопленное значение обратно в LHS (технически связать это в терминах Python)
Если у вас нет оператора with account_balance_lock
и вы выполняете два вызова change_account_balance
параллельно, вы можете в итоге чередовать три подкомпонентных операции опасным образом. Допустим, вы одновременно звоните change_account_balance(100)
(поз. АКА) и change_account_balance(-100)
(отриц. АКА). Это может произойти:
pos = threading.Thread(target=change_account_balance, args=[100])
neg = threading.Thread(target=change_account_balance, args=[-100])
pos.start(), neg.start()
- pos: читать текущее значение -> 0
- neg: прочитать текущее значение -> 0
- pos: добавить текущее значение для чтения значения -> 100
- neg: добавить текущее значение для чтения значения -> -100
- pos: записать текущее значение -> account_balance = 100
- neg: записать текущее значение -> account_balance = -100
Поскольку вы не заставляли операции выполняться в отдельных фрагментах, у вас может быть три возможных результата (-100, 0, 100).
Оператор with [lock]
является единственной неделимой операцией, которая говорит: «Позвольте мне быть единственным потоком, выполняющим этот блок кода. Если что-то еще выполняется, это круто - я подожду». Это гарантирует, что обновления account_balance
являются «поточно-ориентированными» (параллелизм-безопасными).
Примечание: В этой схеме есть предостережение: вы должны помнить, чтобы получать account_balance_lock
(через with
) каждый раз, когда вы хотите манипулировать account_balance
, чтобы код оставался потокобезопасный. Есть способы сделать это менее хрупким, но это ответ на совершенно другой вопрос.
Редактировать: В ретроспективе, вероятно, важно упомянуть, что оператор with
неявно вызывает блокировку acquire
блокировки - это часть "Я подожду" выше диалог темы. Напротив, неблокирующее приобретение говорит: «Если я не могу получить блокировку сразу, дайте мне знать», а затем полагается на вас, чтобы проверить, получили ли вы блокировку или нет.
import logging # This module is thread safe.
import threading
LOCK = threading.Lock()
def run():
if LOCK.acquire(False): # Non-blocking -- return whether we got it
logging.info('Got the lock!')
LOCK.release()
else:
logging.info("Couldn't get the lock. Maybe next time")
logging.basicConfig(level=logging.INFO)
threads = [threading.Thread(target=run) for i in range(100)]
for thread in threads:
thread.start()
Я также хочу добавить, что основная цель блокировки состоит в том, чтобы гарантировать атомарность получения (неделимость acquire
между потоками), что не гарантирует простой логический флаг. Семантика атомарных операций, вероятно, также является содержанием другого вопроса.