Какие проблемы возникают при одновременном доступе нескольких потоков к общей переменной

Ответ

Основная проблема — состояние гонки (race condition). Она возникает, когда результат выполнения кода зависит от непредсказуемой последовательности операций в нескольких потоках, одновременно обращающихся к общим данным.

Чаще всего это происходит из-за неатомарных операций. Например, операция инкремента (counter += 1) на самом деле состоит из трех шагов:

  1. Чтение текущего значения переменной.
  2. Увеличение значения на 1.
  3. Запись нового значения обратно в переменную.

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

Пример на Python, демонстрирующий race condition:

import threading

# Общая глобальная переменная
counter = 0

def increment():
    global counter
    for _ in range(100000):
        # Критическая секция: неатомарная операция
        counter += 1

threads = [threading.Thread(target=increment) for _ in range(10)]

for t in threads:
    t.start()

for t in threads:
    t.join()

# Ожидаемый результат: 1_000_000
# Реальный результат будет меньше из-за потерянных инкрементов
print(f"Финальное значение счетчика: {counter}")

Другие связанные проблемы:

  • Deadlock (взаимная блокировка): Два или более потока вечно ожидают друг друга, чтобы освободить ресурсы, которые они заблокировали.
  • Неконсистентность данных: Чтение данных в промежуточном, невалидном состоянии, пока другой поток их изменяет.

Решение:

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

Исправленный пример с threading.Lock:

import threading

counter = 0
lock = threading.Lock()

def safe_increment():
    global counter
    for _ in range(100000):
        with lock: # Блокировка на время выполнения критической секции
            counter += 1

# ... (создание и запуск потоков с target=safe_increment)
# ...
# print(counter) -> Выведет корректный результат 1_000_000

Ответ 18+ 🔞

Ах ты ж ёпта, классика жанра, блядь! Состояние гонки, сука! Это когда твои потоки, как мартышлюшки на банановой плантации, одновременно лезут в одну и ту же переменную, а потом ты сидишь и думаешь: "А хули у меня результат не сходится, я же всё правильно написал?"

Вот смотри, в чём пиздец. Кажется, что counter += 1 — это одна операция. Ан нет, блядь! Это целых три шага, как в плохом анекдоте: прочитать, прибавить, записать. И если два потока это делают в одно и то же время, один из них может спокойно перезаписать результат другого, и инкремент, блядь, теряется. Вот тебе и волнение ебать, когда в логах цифры не бьются.

Вот, на, посмотри на этот говнокод, он всё наглядно покажет:

import threading

# Общая глобальная переменная
counter = 0

def increment():
    global counter
    for _ in range(100000):
        # Критическая секция: неатомарная операция
        counter += 1

threads = [threading.Thread(target=increment) for _ in range(10)]

for t in threads:
    t.start()

for t in threads:
    t.join()

# Ожидаемый результат: 1_000_000
# Реальный результат будет меньше из-за потерянных инкрементов
print(f"Финальное значение счетчика: {counter}")

Запустишь — и охуеешь. Вместо честного миллиона получишь какую-то рандомную хуйню, потому что потоки друг другу всё время мешали, как пьяные в подъезде.

А ещё, блядь, есть и другие приколы. Deadlock, например. Это когда два потока друг друга ждут, как идиоты, и оба нихуя не делают. Или неконсистентность данных — читаешь переменную, а она в каком-то промежуточном, кривом состоянии, потому что другой поток её в этот момент пилит.

Что делать-то, спрашиваешь? Да синхронизировать их, этих распиздяев! Поставить мьютекс, как будку с охранником. Пусть по одному заходят в критическую секцию и делают свои делишки, не мешая другим.

Вот, смотри, как надо:

import threading

counter = 0
lock = threading.Lock()

def safe_increment():
    global counter
    for _ in range(100000):
        with lock: # Блокировка на время выполнения критической секции
            counter += 1

# ... (создание и запуск потоков с target=safe_increment)
# ...
# print(counter) -> Выведет корректный результат 1_000_000

Вот теперь всё чётко. Один поток зашёл под with lock, сделал свои три шага, вышел — следующий заходит. Порядок, блядь, а не бардак. И результат будет ровно миллион, как и задумывалось. А то без лока — чистая лотерея, в рот меня чих-пых.