С C++11 потоки и модель памяти часть стандарта. На собесе middle и senior почти всегда спрашивают про гонки, мьютексы, условные переменные и атомики, а иногда про memory_order.
Потоки
std::thread t([] { work(); });
t.join(); // дождаться завершения
std::jthread jt([](std::stop_token st) { while (!st.stop_requested()) step(); });
Если объект std::thread уничтожается в состоянии joinable (не вызвали join или detach), вызывается std::terminate. std::jthread (C++20) в деструкторе запрашивает остановку и делает join сам. detach отпускает поток в свободное плавание: он не должен использовать ссылки на локальные переменные создателя.
Гонка данных
Гонка данных (data race): два потока обращаются к одной ячейке памяти без синхронизации, и хотя бы один пишет. По стандарту это неопределённое поведение всей программы, а не просто «неточный счётчик». Даже ++counter для int это чтение, сложение и запись, которые могут перемешаться между потоками и потерять обновления. volatile от гонок не защищает: оно про оптимизации доступа к памяти, а не про синхронизацию.
Мьютексы
std::mutex m;
{
std::lock_guard lock(m); // RAII: захват в конструкторе, освобождение в деструкторе
shared.push_back(x);
}
std::lock_guardпростой RAII-замок;std::unique_lockумеет отпускать и захватывать заново, нужен для условных переменных;std::scoped_lock(C++17) захватывает несколько мьютексов сразу с алгоритмом избегания взаимной блокировки.std::shared_mutex+std::shared_lockдля «много читателей, один писатель».- Дедлок возникает, когда потоки захватывают мьютексы в разном порядке. Лечится единым порядком захвата или
std::scoped_lock(a, b).
Условные переменные
std::unique_lock lock(m);
cv.wait(lock, [&] { return !queue.empty() || stopped; });
wait атомарно отпускает мьютекс и засыпает, а проснувшись, снова его захватывает. Пробуждения бывают ложными (spurious), и уведомление может прийти до того, как поток начал ждать, поэтому всегда ждут с предикатом, проверяющим состояние под мьютексом. Изменять это состояние тоже нужно под тем же мьютексом.
Атомики
std::atomic<T> делает операции над одной переменной неделимыми: load, store, fetch_add, exchange, compare_exchange_weak/strong (CAS). Для простых счётчиков и флагов это быстрее мьютекса. is_lock_free() показывает, реализован ли атомик без внутренней блокировки. Атомарность одной операции не делает атомарной последовательность: if (x.load() > 0) x--; всё ещё гонка логики, нужен CAS-цикл.
Модель памяти и memory_order
Компилятор и процессор переставляют операции с памятью. Модель памяти определяет, какие перестановки видны другим потокам, через отношение happens-before.
memory_order_seq_cst(по умолчанию): все атомарные операции выстроены в единый глобальный порядок. Проще всего рассуждать, иногда дороже.memory_order_releaseпри записи иmemory_order_acquireпри чтении того же атомика: если чтение увидело записанное значение, всё, что поток-писатель сделал до release, видно потоку-читателю после acquire. Классика: публикация данных через флаг.memory_order_relaxed: только атомарность самой операции, никакого порядка с остальной памятью. Годится для статистических счётчиков.
data = 42; // обычная запись
ready.store(true, std::memory_order_release);
// другой поток:
while (!ready.load(std::memory_order_acquire)) {}
assert(data == 42); // гарантировано
Высокоуровневые средства
std::async и std::future возвращают результат из другого потока (исключения тоже передаются). С политикой по умолчанию задача может выполниться отложенно в момент get(), а деструктор future от std::async ждёт завершения задачи. Для пулов потоков стандартной библиотеки нет, их пишут сами или берут готовые.
Частые ошибки
- Общая переменная без мьютекса или атомика.
waitбез предиката и изменение условия без мьютекса.- Разный порядок захвата мьютексов.
- Вызов чужого кода (колбэка) под мьютексом.
detachс захватом локальных переменных по ссылке.- Двойная проверка без атомика при ленивой инициализации (используйте
static-локальные переменные илиstd::call_once).
Что спросят на собесе
- Что такое гонка данных и почему это UB.
- Как избежать дедлока.
- Зачем предикат в
condition_variable::wait. - Чем атомик отличается от мьютекса, что такое CAS.
memory_orderacquire/release и relaxed на примере.