Что происходит, если несколько потоков пытаются изменить одну и ту же переменную одновременно?

При одновременном изменении одной и той же переменной несколькими потоками может возникнуть состояние гонки (race condition). Это может привести к непредсказуемым результатам, таким как потеря данных, повреждение данных или некорректное состояние программы. Для избежания этого необходимо использовать механизмы синхронизации, такие как блокировки (locks), семафоры (semaphores) или атомарные операции.

Когда несколько потоков пытаются одновременно изменить одну и ту же переменную, возникают проблемы, связанные с состоянием гонки (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}")
  
0