Многопоточность — стандартная модель памяти и переносимые примитивы
Процессоры давно перестали ускоряться за счёт тактовой частоты — производители добавляют ядра, и выжать из них пользу без явной многопоточности не получится. Но многопоточность в C++ это не библиотечная фича, а пересечение языка, компилятора и процессора. C++11 впервые дал стандартную модель памяти и переносимые примитивы — <thread>, <mutex>, <atomic>, <condition_variable>, <future>. До этого каждый порт под Windows / Linux / macOS требовал своего набора платформенных API.
Главное правило, на котором всё держится: одновременный доступ двух потоков к одной переменной, где хотя бы один поток пишет, без синхронизации — это data race, а data race — это Undefined Behavior. Не «неправильный результат», а UB: компилятор и процессор имеют право переупорядочить операции, держать значения в регистрах, объединять записи. Поэтому «у меня обычно правильно считает» ничего не доказывает.
Важно отличать concurrency (программа умеет вести несколько задач вперемешку, не блокируя прогресс) от parallelism (задачи реально выполняются одновременно на разных ядрах). C++ даёт инструменты для обоих, но требует, чтобы разработчик явно проговорил порядок видимости записей — через мьютексы (sequenced-before внутри критической секции) или через memory_order на атомиках. Полная карта — в слоях ниже.
Карта темы
- Потоки —
std::thread,std::jthread,join/detach, передача аргументов, стек потока,thread_local. - Синхронизация —
mutex,lock_guard/unique_lock/scoped_lock,shared_mutex,condition_variable, deadlock,latch/barrier/semaphoreиз C++20. - Атомики и memory_order —
std::atomic,compare_exchange, ABA, шесть значенийmemory_order,atomic_flag, когда не писать lock-free. - future, promise и async —
std::future/promise/packaged_task/async,shared_future, проброс исключений между потоками. - Время и ожидание —
std::chrono,steady_clockvssystem_clock,sleep_for/sleep_until, таймауты в синхронизации.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Доступ к общим данным без синхронизации | Data race → UB; компилятор может выкинуть «лишние» load/store |
Уничтожить joinable std::thread без join()/detach() | std::terminate() в деструкторе потока |
cv.wait(lk) без предиката | Spurious wakeup → код выполнится, когда условие ещё не наступило |
std::async без std::launch::async | Может выполниться синхронно в f.get() — нет параллелизма |
Считать volatile атомиком | Гонки сохраняются; volatile про MMIO/signal, не про потоки |
| Захватить несколько мьютексов в разном порядке | Deadlock; используйте std::scoped_lock или общий порядок захвата |
memory_order_relaxed для публикации указателя | Reader увидит указатель раньше, чем данные за ним — UB |
detach() поток, ссылающийся на локалы родителя | Dangling refs во frame после возврата из родительской функции |
shared_ptr<T> без std::atomic<shared_ptr<T>> для гонки | Гонка по control block; в C++20 есть атомарные перегрузки |
Использовать system_clock для измерения интервалов | Часы могут «прыгнуть» из-за NTP/смены пользователя; нужен steady_clock |
Значение для собеседований
Многопоточность — обязательный блок на интервью middle/senior. Проверяется не «знаешь ли ты std::thread», а понимаешь ли модель памяти, UB и happens-before.
Что обычно проверяют:
- Что такое data race и почему это UB, а не «неправильное число».
- Разница между
lock_guard,unique_lock,scoped_lock— когда что нужно. - Зачем
condition_variable::waitпринимает предикат (spurious wakeup, lost notification). - Что произойдёт с joinable
std::threadв деструкторе. - Разница между concurrency и parallelism.
- Чем
volatileотличается отstd::atomic. memory_order_release/acquireи happens-before — для senior.- False sharing и его влияние на производительность.
Типичный неверный ответ: «Поставлю volatile — будет потокобезопасно». Это запускает обсуждение того, что volatile запрещает компилятору только убирать обращения и переупорядочивать их внутри одного потока, но ничего не говорит о видимости между потоками. Для синхронизации нужен std::atomic или мьютекс.
Популярные задачи: «напишите потокобезопасную очередь» (mutex + condition_variable + предикат); «реализуйте spinlock на atomic_flag»; «объясните, что делает compare_exchange_weak в цикле и зачем weak».