Когда несколько потоков пытаются одновременно изменить одну и ту же переменную, возникают проблемы, связанные с состоянием гонки (race condition).
Представьте, что переменная - это ячейка памяти, в которой хранится значение. Вот что может случиться:
- Потерянные обновления: Один поток читает значение переменной. Другой поток читает то же значение переменной. Оба потока изменяют значение (например, увеличивают на 1). Оба потока записывают свои новые значения обратно в переменную. В зависимости от времени, когда каждый поток записывает, одно из обновлений может быть потеряно. То есть, оба потока увеличили значение на 1, но конечное значение может увеличиться только на 1 вместо 2.
- Неконсистентные данные: Процесс изменения переменной может состоять из нескольких операций (например, чтение, изменение, запись). Если потоки чередуются между этими операциями, переменная может оказаться в промежуточном, неконсистентном состоянии. Другие потоки, читающие переменную в этот момент, могут получить неверные данные.
- Deadlock (в более сложных сценариях): Хотя прямое изменение одной переменной редко приводит к deadlock, гонка за доступ к переменной может быть частью более сложной ситуации, где потоки заблокированы, ожидая друг друга освободить ресурсы.
Чтобы предотвратить эти проблемы, необходимо использовать механизмы синхронизации, такие как:
- Locks (Mutexes): Поток получает блокировку (lock) перед тем, как изменить переменную, и освобождает блокировку после завершения. Только один поток может владеть блокировкой одновременно, что гарантирует эксклюзивный доступ к переменной. В Python это реализуется с помощью модуля `threading.Lock` или `threading.RLock` (reentrant lock).
- Semaphores: Похожи на locks, но позволяют ограниченному числу потоков одновременно получать доступ к ресурсу. В Python: `threading.Semaphore`.
- Atomic operations: Некоторые операции могут быть атомарными, то есть они выполняются как единое неделимое действие. Однако в Python атомарность не гарантируется для всех операций, особенно сложных.
- Queue: Использовать потокобезопасную очередь (`queue.Queue`) для передачи данных между потоками. Вместо того чтобы напрямую изменять общую переменную, потоки помещают задачи или данные в очередь, а один поток (потребитель) обрабатывает их последовательно.
Выбор конкретного механизма синхронизации зависит от конкретной ситуации и требований к производительности. Важно тщательно проектировать многопоточный код, чтобы избежать гонок и обеспечить корректную работу.
Пример использования Lock:
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
with lock: # Блокировка устанавливается при входе в блок 'with' и освобождается при выходе
temp = counter
temp = temp + 1
counter = temp
threads = []
for _ in range(10):
t = threading.Thread(target=increment)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Counter value: {counter}")