← Назад к вопросам
Что будет при обращении двух потоков к одному участку памяти?
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!
}
};
Выводы
При обращении двух потоков к одному участку памяти:
- Без синхронизации — undefined behavior, crash, потеря данных
- Результат непредсказуем — зависит от timing
- Нужна синхронизация:
- Mutex — для больших критичных секций
- Atomic — для простых переменных
- Lock-Free structures — для максимальной производительности
Золотое правило: Если несколько потоков обращаются к одним данным, и хотя бы один пишет — нужна синхронизация!