← Назад к вопросам

Что будет при обращении двух потоков к одному участку памяти?

2.3 Middle🔥 171 комментариев
#Многопоточность и синхронизация#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что происходит при обращении двух потоков к одному участку памяти?

Это одна из самых критичных проблем в многопоточном программировании — race condition. При одновременном доступе двух потоков к одним данным результат непредсказуем.

Проблема: Data Race

#include <iostream>
#include <thread>

int shared_counter = 0;

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        shared_counter++;  // RACE CONDITION!
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    // Ожидаем: 2,000,000
    // Получим: 1,543,827 (или какой-то другой результат)
    std::cout << "Counter: " << shared_counter << std::endl;
    
    return 0;
}

Почему это происходит?

Операция counter++ — это НЕ атомарная операция!

На уровне CPU это три отдельных инструкции:

// shared_counter++ раскрывается в:
// 1. LOAD: r0 = memory[&shared_counter]
// 2. ADD:  r0 = r0 + 1
// 3. STORE: memory[&shared_counter] = r0

// Если два потока выполняют это одновременно:
Thread 1                        Thread 2
LOAD  r0 = 100  -------        /------ LOAD r0 = 100
ADD   r0 = 101  -----  \      /  ----- ADD  r0 = 101
STORE [addr]=101 --- \/\/\--- -------- STORE [addr]=101

// Результат: counter = 101 (вместо 102!)

Различные типы data race

1. Read-Write Race (чтение-запись)

int value = 0;

void writer() {
    value = 42;  // WRITE
}

void reader() {
    int x = value;  // READ
    std::cout << x << std::endl;
}

// Может прочитать: 0 или 42, в зависимости от timing!

2. Write-Write Race (запись-запись)

int value = 0;

void writer1() {
    value = 10;  // WRITE
}

void writer2() {
    value = 20;  // WRITE
}

// Кто выпишет последним? Непредсказуемо!

3. Read-Read Race (обычно безопасно)

int value = 42;

void reader1() {
    int x = value;  // READ - безопасно
}

void reader2() {
    int y = value;  // READ - безопасно
}

// Это безопасно!

Реальные последствия

#include <iostream>
#include <thread>

struct Node {
    int data;
    Node* next;
};

Node* list_head = nullptr;

void insert(int value) {
    Node* new_node = new Node{value, nullptr};
    new_node->next = list_head;
    list_head = new_node;  // RACE CONDITION!
}

void remove() {
    if (list_head) {
        Node* temp = list_head;
        list_head = list_head->next;
        delete temp;  // Может быть double-delete!
    }
}

int main() {
    std::thread t1([]{ insert(1); insert(2); });
    std::thread t2([]{ remove(); remove(); });
    
    t1.join();
    t2.join();
    
    // Вероятные проблемы: double-delete, use-after-free
    return 0;
}

Решение 1: Mutex (мьютекс)

#include <iostream>
#include <thread>
#include <mutex>

int shared_counter = 0;
std::mutex counter_mutex;

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        {
            std::lock_guard<std::mutex> lock(counter_mutex);
            shared_counter++;  // Безопасно!
        }
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << shared_counter << std::endl;  // 2,000,000
    return 0;
}

Решение 2: Atomic (атомарные операции)

#include <iostream>
#include <thread>
#include <atomic>

std::atomic<int> shared_counter(0);

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        shared_counter++;  // Атомарная, безопасна!
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << shared_counter << std::endl;  // 2,000,000
    return 0;
}

Решение 3: Lock-Free Data Structures

#include <atomic>
#include <memory>

template <typename T>
class LockFreeStack {
    struct Node {
        T data;
        Node* next;
    };
    
    std::atomic<Node*> head;
    
public:
    void push(const T& value) {
        Node* new_node = new Node{value, nullptr};
        
        Node* old_head;
        do {
            old_head = head.load();
            new_node->next = old_head;
        } while (!head.compare_exchange_weak(old_head, new_node));
    }
    
    bool pop(T& result) {
        Node* old_head;
        Node* new_head;
        
        do {
            old_head = head.load();
            if (old_head == nullptr) return false;
            new_head = old_head->next;
        } while (!head.compare_exchange_weak(old_head, new_head));
        
        result = old_head->data;
        delete old_head;
        return true;
    }
};

Отладка Race Conditions

ThreadSanitizer (TSAN):

# Компилировать с TSAN
g++ -fsanitize=thread -g main.cpp -o app

# TSAN найдёт race conditions
./app

Helgrind:

valgrind --tool=helgrind ./app

Best Practices

// ПРАВИЛЬНО: используй synchronization primitives
class Bank {
    std::mutex balance_mutex;
    double balance = 0.0;
    
public:
    void transfer(double amount) {
        std::lock_guard<std::mutex> lock(balance_mutex);
        balance += amount;
    }
};

// НЕПРАВИЛЬНО: надеешься на атомарность
class BadBank {
    long balance = 0;  // Может быть не атомарен!
    
    void transfer(long amount) {
        balance += amount;  // RACE CONDITION!
    }
};

Выводы

При обращении двух потоков к одному участку памяти:

  1. Без синхронизации — undefined behavior, crash, потеря данных
  2. Результат непредсказуем — зависит от timing
  3. Нужна синхронизация:
    • Mutex — для больших критичных секций
    • Atomic — для простых переменных
    • Lock-Free structures — для максимальной производительности

Золотое правило: Если несколько потоков обращаются к одним данным, и хотя бы один пишет — нужна синхронизация!

Что будет при обращении двух потоков к одному участку памяти? | PrepBro