Многопоточность
Потоки, синхронизация, атомики, future и типичные ошибки конкурентного кода.
43 вопросов
JuniorТеорияОчень частоЧто такое атомарная операция?
Что такое атомарная операция?
Атомарная операция завершается без видимого промежуточного состояния — другие потоки видят её как полностью выполненную или не начатую. std::atomic<T> делает load, store, fetch_add и compare_exchange неделимыми.
Типичные ошибки
- ✗Считать
i++на не-атомарном целом атомарным на x86 — операция чтение-изменение-запись состоит из трёх отдельных обращений к памяти - ✗Использовать
std::atomicдля структуры и ожидать, что обновление всей структуры атомарно — только типы сis_lock_freeдействительно без блокировок - ✗Думать, что атомарные операции имеют нулевую стоимость — инструкции LOCK и трафик когерентности кэша могут быть дорогостоящими во внутренних циклах
Уточняющие вопросы
- →В чём разница
compare_exchange_weakиcompare_exchange_strongи когда использовать каждый? - →Чем
std::atomic<T>::fetch_addотличается отx += nв многопоточном контексте?
JuniorТеорияОчень частоКак использовать std::mutex? RAII-обёртки.
Как использовать std::mutex? RAII-обёртки.
Используйте RAII-обёртки — никогда lock()/unlock() вручную: lock_guard простейший, unique_lock мобильный и нужен для condition_variable, scoped_lock (C++17) захватывает несколько мьютексов атомарно.
Типичные ошибки
- ✗Напрямую вызывать
mutex.lock(), а затем бросать исключение — блокировка никогда не снимается, дедлок гарантирован - ✗Использовать
lock_guard, когда мьютекс нужно разблокировать до конца области видимости — используйтеunique_lockс явнымunlock() - ✗Создавать мьютекс как локальную переменную функции, вызываемой из нескольких потоков — каждый вызов получает свой мьютекс, синхронизации нет
Уточняющие вопросы
- →Что такое
std::scoped_lockи как он предотвращает дедлок при захвате нескольких мьютексов? - →Что происходит при перемещении
std::unique_lock, удерживающего блокировку?
JuniorТеорияОчень частоЧто такое гонка данных? Как её избежать? Что такое критическая секция?
Что такое гонка данных? Как её избежать? Что такое критическая секция?
Гонка данных — конкурентный доступ к одной памяти с хотя бы одной записью без синхронизации — UB. Состояние гонки — более широкий баг: корректность зависит от планирования. Критическая секция должна выполняться атомарно.
Типичные ошибки
- ✗Исправлять гонку данных, делая переменную
volatile—volatileне обеспечивает упорядочивание памяти - ✗Захватывать несколько мьютексов в разном порядке в разных потоках — классическая подготовка к дедлоку
- ✗Использовать
if (!flag) { flag = true; doWork(); }без атомиков — проверка и установка флага не атомарна
Уточняющие вопросы
- →Что такое ThreadSanitizer и как он обнаруживает гонки данных во время выполнения?
- →Как
std::atomic<bool>устраняет гонку в примере с флагом выше?
JuniorТеорияОчень частоКак синхронизировать передачу данных между потоками?
Как синхронизировать передачу данных между потоками?
Mutex с lock_guard/unique_lock для защиты общих данных; condition_variable для producer-consumer; std::atomic для одной переменной; std::promise/future для однократной передачи.
Типичные ошибки
- ✗Захватывать мьютекс и затем засыпать внутри критической секции — удерживает блокировку пока заблокирован, голодающие другие потоки
- ✗Использовать отдельные мьютексы для связанных данных — нужно всегда захватывать оба в одном порядке или рискуете дедлоком
- ✗Использовать
volatileвместоstd::atomicдля межпоточных флагов —volatileне обеспечивает упорядочивание памяти
Уточняющие вопросы
- →В чём разница между
std::lock_guardиstd::unique_lock? - →Когда вы использовали бы
std::shared_mutexвместо обычногоstd::mutex?
JuniorТеорияОчень частоЧто произойдёт, если std::thread уничтожить без join или detach?
Что произойдёт, если std::thread уничтожить без join или detach?
Если объект std::thread остаётся joinable в момент работы деструктора, программа немедленно вызывает std::terminate(). Нужно явно вызвать join или detach до уничтожения.
Типичные ошибки
- ✗Позволить исключению пропустить join
- ✗Использовать detach, когда поток всё ещё обращается к стековым переменным
- ✗Путать время жизни объекта std::thread с завершением OS-потока
Уточняющие вопросы
- →Чем здесь помогает std::jthread?
- →Когда detach допустим?
JuniorТеорияОчень частоЯвляется ли C++ потокобезопасным? Какие гарантии даёт стандарт?
Является ли C++ потокобезопасным? Какие гарантии даёт стандарт?
C++ гарантирует конкурентное чтение одного объекта, конкурентные вызовы на разных объектах одного типа, операции std::atomic и потокобезопасную инициализацию static-локальных (C++11). Конкурентные чтение+запись одного не-атомарного объекта — гонка данных и UB.
Типичные ошибки
- ✗Думать, что
constозначает потокобезопасность —const-метод всё равно может вызыватьmutable-члены с гонками данных - ✗Обращаться к
std::coutиз нескольких потоков — до C++20 это только частично безопасно (символы могут перемежаться) - ✗Считать операции STL-контейнеров атомарными — они не являются; конкурентный
push_backбез мьютекса — UB
Уточняющие вопросы
- →Как модель памяти C++11 формально определяет 'гонку данных'?
- →В чём заключается принцип дизайна
constи потокобезопасности от Herb Sutter?
MiddleТеорияОчень частоКак использовать std::condition_variable? Что такое spurious wakeup?
Как использовать std::condition_variable? Что такое spurious wakeup?
Захватите unique_lock и вызовите cv.wait(lock, predicate) — он атомарно освобождает блокировку и блокируется. При notify поток повторно захватывает блокировку и проверяет предикат. Spurious wakeup — wait вернулся без notify.
Типичные ошибки
- ✗Использовать
cv.wait(lock)без предиката — spurious wakeups заставляют поток продолжить выполнение, когда условие не выполнено - ✗Вызывать
notify_one/notify_allбез удержания мьютекса — может вызвать потерянные уведомления в редких временных окнах - ✗Использовать
condition_variableсlock_guard—waitтребуетunique_lock, потому что должен разблокировать/перезахватить
Уточняющие вопросы
- →В чём разница между
notify_oneиnotify_allи когда использовать каждый? - →Как использовать
condition_variable::wait_forдля реализации ожидания с таймаутом?
MiddleТеорияОчень частоЧто такое deadlock и как его предотвращать?
Что такое deadlock и как его предотвращать?
Deadlock — когда потоки бесконечно ждут друг друга, обычно из-за захвата мьютексов в разном порядке. Предотвращают единым порядком захвата, короткими scope и std::scoped_lock для нескольких мьютексов.
Типичные ошибки
- ✗Захватывать A затем B в одном пути и B затем A в другом
- ✗Вызывать пользовательский код под mutex
- ✗Держать lock во время блокирующего I/O
Уточняющие вопросы
- →Что делает std::scoped_lock с несколькими mutex?
- →Что такое livelock?
MiddleТеорияОчень частоКакие примитивы синхронизации предоставляет C++? Преимущества lock_guard.
Какие примитивы синхронизации предоставляет C++? Преимущества lock_guard.
Мьютексы, блокировки (lock_guard/unique_lock/scoped_lock), condition_variable, атомики и C++20 latch/barrier/semaphore. lock_guard — простейшая RAII-обёртка: блокирует при конструировании, освобождает при уничтожении.
Типичные ошибки
- ✗Использовать
unique_lockвезде 'для безопасности' — у него есть флагbool lockedи дополнительные накладные расходы;lock_guardпроще и дешевле - ✗Не знать, что
scoped_lockсуществует, и вручную реализовывать захват нескольких мьютексов с потенциальным дедлоком - ✗Забывать, что
lock_guardнельзя перемещать — он привязан к области видимости, в которой создан
Уточняющие вопросы
- →Когда
std::scoped_lock<M1, M2>гарантирует захват двух мьютексов без дедлока? - →Можно ли использовать
lock_guardсtimed_mutex?
JuniorТеорияЧастоВ чём разница между concurrency и parallelism?
В чём разница между concurrency и parallelism?
Concurrency — структурирование программы так, чтобы задачи продвигались в пересекающиеся промежутки времени. Parallelism — буквально одновременное выполнение задач на разных ядрах. Concurrent может работать параллельно, но не обязан.
Типичные ошибки
- ✗Использовать термины как синонимы
- ✗Считать, что больше потоков всегда быстрее
- ✗Игнорировать стоимость синхронизации
Уточняющие вопросы
- →Как выбрать число worker-потоков?
- →Что такое логическое ядро CPU?
JuniorТеорияЧастоВ чём разница между мьютексом и семафором?
В чём разница между мьютексом и семафором?
Мьютекс имеет владельца — освободить может только захвативший. Семафор — сигнальный примитив со счётчиком; сигналить может любой поток. C++20 добавляет std::counting_semaphore/binary_semaphore.
Типичные ошибки
- ✗Использовать мьютекс там, где нужен семафор для межпоточной сигнализации — мьютекс может разблокировать только его владелец
- ✗Забывать вызывать
V()на всех путях кода — счётчик семафора остаётся низким и другие потоки блокируются навсегда (утечка семафора) - ✗Использовать семафор со счётчиком > 1 для защиты одного ресурса — используйте мьютекс вместо этого
Уточняющие вопросы
- →Как реализовать ограниченную очередь производитель-потребитель с использованием считающего семафора?
- →Что такое condition variable и как оно соотносится с семафором для паттернов ожидания/уведомления?
MiddleТеорияЧастоstd::launch::async vs std::launch::deferred. Как работает std::async?
std::launch::async vs std::launch::deferred. Как работает std::async?
launch::async запускает задачу сразу в новом потоке; launch::deferred лениво — в вызывающем потоке при get(). Умолчание может молча оказаться deferred, а деструктор future блокирует при async.
Типичные ошибки
- ✗Не указывать политику запуска и полагаться на умолчание — может выполниться deferred, когда ожидался async
- ✗Отбрасывать возвращённый future — с
launch::asyncдеструктор блокирует до завершения задачи, создавая непреднамеренный join - ✗Ожидать, что
asyncвсегда создаёт новый поток — реализации могут использовать ограниченный пул потоков
Уточняющие вопросы
- →Как отменить async-задачу, запущенную через
std::async? - →В чём разница между
std::packaged_taskиstd::asyncс точки зрения контроля?
MiddleТеорияЧастоВ чём разница между многопоточностью и асинхронностью?
В чём разница между многопоточностью и асинхронностью?
Многопоточность запускает потоки ОС параллельно на ядрах. Асинхронность стартует задачу и получает результат позже, возможно в том же потоке через event loop. Потоки — физический параллелизм.
Типичные ошибки
- ✗Считать, что
std::asyncвсегда запускается в новом потоке —launch::deferredзапускается в вызывающем потоке - ✗Не вызывать
get()дляstd::future, возвращённогоstd::async— деструктор future блокирует до завершения задачи (дляlaunch::async) - ✗Использовать корутины и считать, что корутина выполняется параллельно — корутина возобновляется в потоке, который вызывает
resume()
Уточняющие вопросы
- →В чём разница между
std::asyncиstd::thread+std::promise? - →Как корутины C++20 реализуют кооперативную многозадачность без потоков?
MiddleТеорияЧастоГарантии потокобезопасности контейнеров STL. Почему front() + pop_front() небезопасен?
Гарантии потокобезопасности контейнеров STL. Почему front() + pop_front() небезопасен?
Конкурентные чтения безопасны; любая конкурентная запись — гонка данных. front() + pop_front() — гонка TOCTOU: поток A держит ссылку, а B вызывает pop_front() и уничтожает элемент.
Типичные ошибки
- ✗Использовать отдельный мьютекс для проверки и другой для изменения — окно TOCTOU между освобождением и повторным захватом
- ✗Вызывать
size()внутри условного выражения и затем изменять на основе результата без удержания блокировки через оба — классическая гонка check-then-act - ✗Думать, что
std::atomic<int>как размер контейнера достаточен — внутреннее состояние контейнера по-прежнему имеет гонки
Уточняющие вопросы
- →Как реализовать потокобезопасную очередь, поддерживающую
try_dequeueбез блокировки? - →Что такое проблема ABA в lock-free очередях и как её предотвратить?
MiddleТеорияЧастоЧто даёт std::counting_semaphore, чего нет у std::mutex?
Что даёт std::counting_semaphore, чего нет у std::mutex?
std::counting_semaphore (C++20) хранит счётчик, поэтому допускает до N одновременных держателей, а не одного. У него нет владельца — любой поток может вызвать release() для пермита, захваченного другим потоком, что подходит для межпоточной сигнализации и пулов ресурсов.
Типичные ошибки
- ✗Использовать семафор там, где подходит мьютекс — теряя проверки владения и порядка блокировок, на которые опираются инструменты
- ✗Забыть
release()на пути ошибки, навсегда уменьшая число пермитов (утечка семафора) - ✗Брать
counting_semaphore<1>для взаимного исключения вместо более ясногоbinary_semaphore
Уточняющие вопросы
- →Как построить ограниченный буфер на двух counting-семафорах?
- →Какова роль шаблонного параметра
LeastMaxValue?
MiddleТеорияЧастоКакие средства многопоточности предоставляет C++? Частые ловушки.
Какие средства многопоточности предоставляет C++? Частые ловушки.
C++11 добавил std::thread, мьютексы, condition_variable, атомики, future/async. C++17 — shared_mutex и параллельный STL. C++20 — jthread, latch, barrier, semaphore, корутины.
Типичные ошибки
- ✗Уничтожать
std::threadбез join или detach — вызываетstd::terminate - ✗Вызывать
std::async(std::launch::async, ...)и отбрасывать возвращённый future — деструктор future блокируется - ✗Использовать
std::execution::parдля алгоритмов, изменяющих общее состояние — параллелизм создаёт гонку данных
Уточняющие вопросы
- →Чем
std::jthreadотличается отstd::threadв плане управления жизненным циклом? - →Что такое
std::latchиstd::barrierи чем они отличаются?
MiddleДебаггингЧастоПочему этот многопоточный счётчик неверен и как это исправить?
Почему этот многопоточный счётчик неверен и как это исправить?
Гонка данных: ++counter — несинхронизированная операция «чтение-изменение-запись» между потоками, что является неопределённым поведением, поэтому итоговое значение непредсказуемо. Решение: std::atomic<int> counter{0} (тогда ++counter атомарен) или защитить инкремент std::mutex + lock_guard.
Типичные ошибки
- ✗Считать
++атомарным, потому что это один оператор или одна инструкция - ✗Думать, что
volatileобеспечивает потокобезопасность - ✗Полагать, что потерянные обновления лишь замедляют, а не портят результат
Уточняющие вопросы
- →Почему
volatileне заменяетstd::atomicв C++? - →Когда для счётчика
std::mutexпредпочтительнееstd::atomic?
MiddleТеорияЧастоЧто добавляет C++20 std::jthread сверх std::thread?
Что добавляет C++20 std::jthread сверх std::thread?
std::jthread автоматически делает join в деструкторе (без terminate() если забыл) и интегрируется с std::stop_token для кооперативной отмены через request_stop() и stop_requested().
Типичные ошибки
- ✗Смешивать
jthreadи ручнойjoin()в ветвлениях — корректно, но избыточно - ✗Забывать, что
request_stop()— кооперативный; поток должен проверять token - ✗Использовать
std::threadи удивлятьсяterminate(), когда деструктор бежит у joinable-потока
Уточняющие вопросы
- →Как работает
std::stop_callback? - →Можно ли пробрасывать stop в дочерние потоки?
MiddleТеорияЧастоЧто делают std::latch и std::barrier и чем они различаются?
Что делают std::latch и std::barrier и чем они различаются?
Оба (C++20) заставляют потоки ждать, пока счётчик не дойдёт до нуля. std::latch одноразовый: уменьшили счётчик раз — и он исчерпан. std::barrier переиспользуемый: каждый arrive_and_wait сбрасывает его к следующей фазе и запускает completion-функцию между фазами.
Типичные ошибки
- ✗Пытаться переиспользовать
std::latchдля второй фазы — его нельзя сбросить, нуженstd::barrier - ✗Вызывать
count_downбольше раз, чем начальный счётчик, уводя его в минус (UB) - ✗Ожидать, что completion-функция барьера выполнится на фиксированном потоке — её запускает неопределённый прибывший поток
Уточняющие вопросы
- →Когда выбрать
std::barrierвместоstd::condition_variable? - →Что выполняется в completion-функции барьера и на каком потоке?
MiddleДебаггингЧастоПочему этот порядок захвата мьютексов вызывает дедлок и как исправить?
Почему этот порядок захвата мьютексов вызывает дедлок и как исправить?
Дедлок из-за инверсии порядка блокировок: t1 захватывает m1, затем m2; t2 — m2, затем m1. Если каждый возьмёт свой первый мьютекс одновременно, второй не достанется никому. Решение: захватывать в одном глобальном порядке или std::scoped_lock lk(m1, m2) (C++17), запирающий оба атомарно.
Типичные ошибки
- ✗Считать, что короткие критические секции не могут привести к дедлоку
- ✗Путать это с повторным захватом нерекурсивного мьютекса
- ✗Думать, что lock_guard откладывает захват до выхода из области видимости
Уточняющие вопросы
- →Как
std::scoped_lockизбегает дедлока при захвате нескольких мьютексов? - →Что такое дисциплина единого порядка блокировок и почему она работает?
MiddleТеорияЧастоЧто такое модель памяти C++11?
Что такое модель памяти C++11?
Определяет, когда записи одного потока видимы другому через std::atomic с упорядочиваниями (seq_cst, acq_rel, relaxed) и отношением happens-before. До C++11 переносимой модели потоков не было.
Типичные ошибки
- ✗Использовать
memory_order_relaxedдля флага, сигнализирующего готовность других данных — флаг атомарен, но записи данных могут быть невидимы - ✗Думать, что
seq_cstбесплатен — на ARM/Power архитектурах добавляет memory fences, ощутимая стоимость во внутренних циклах - ✗Смешивать C++ атомики с C11 атомиками в одной программе — технически UB из-за разных моделей памяти
Уточняющие вопросы
- →Объясните семантику acquire/release на примере производитель-потребитель.
- →В чём разница между
memory_order_acq_relи использованиемmemory_order_acquireна load +memory_order_releaseна store?
MiddleТеорияЧастоКогда использовать mutex, а когда atomic?
Когда использовать mutex, а когда atomic?
Mutex используйте для защиты инвариантов на нескольких операциях или переменных. Atomic — для простого независимого состояния: счётчиков, флагов. Atomic не делает сложную логику автоматически безопасной.
Типичные ошибки
- ✗Заменять mutex несколькими atomic и ломать инварианты
- ✗Использовать relaxed ordering без доказательства корректности
- ✗Считать, что atomic всегда быстрее
Уточняющие вопросы
- →Что такое memory_order_release/acquire?
- →Почему mutex может быть быстрее при contention?
MiddleТеорияЧастоКаковы особенности std::recursive_mutex?
Каковы особенности std::recursive_mutex?
std::recursive_mutex позволяет одному потоку захватывать блокировку много раз без дедлока. lock() увеличивает внутренний счётчик, unlock() уменьшает; мьютекс освобождается на нуле.
Типичные ошибки
- ✗Использовать recursive_mutex по умолчанию как 'более безопасный' — он тяжелее обычного мьютекса и скрывает проблемы дизайна
- ✗Думать, что recursive_mutex может разблокировать другой поток — у него всё ещё есть владелец; разблокировать может только захвативший поток
- ✗Забывать счётчик блокировок: N захватов требуют ровно N разблокировок; RAII делает подсчёт автоматическим
Уточняющие вопросы
- →Как рефакторировать код, использующий
recursive_mutex, чтобы избежать его необходимости? - →В чём разница стоимости между
std::mutexиstd::recursive_mutexна Linux?
MiddleТеорияЧастоЧто такое read-write мьютекс (std::shared_mutex)?
Что такое read-write мьютекс (std::shared_mutex)?
std::shared_mutex (C++17) имеет shared-режим (чтение) для многих потоков и exclusive-режим (запись) для одного. std::shared_lock для чтения, std::unique_lock для записи.
Типичные ошибки
- ✗Использовать
shared_mutexдля рабочих нагрузок с преобладанием записей — добавляет накладные расходы без преимущества перед обычным мьютексом - ✗Не делать бенчмарк перед переходом с
std::mutexнаstd::shared_mutex— дополнительное отслеживание состояния может быть медленнее при небольшом числе читателей - ✗Использовать один и тот же
shared_mutexсunique_lockиshared_lockв неверном порядке — классический дедлок
Уточняющие вопросы
- →Когда вы использовали бы
std::shared_timed_mutexвместоstd::shared_mutex? - →Как реализовать read-write блокировку используя только
std::mutexиstd::condition_variable?
MiddleТеорияЧастоКак std::scoped_lock захватывает несколько мьютексов без дедлока?
Как std::scoped_lock захватывает несколько мьютексов без дедлока?
Для нескольких мьютексов std::scoped_lock (C++17) вызывает std::lock — алгоритм избегания дедлока: он пробует, откатывается и повторяет при конфликте, поэтому фиксированный порядок захвата не нужен. Затем держит их в стиле RAII и освобождает все в деструкторе.
Типичные ошибки
- ✗Думать, что защита распространяется на мьютексы, захваченные разными объектами
scoped_lock— без дедлока только один совместный вызов - ✗Считать, что
scoped_lockс одним мьютексом всё равно запускает алгоритм отката — он просто работает какlock_guard - ✗Захватить мьютекс вручную и затем передать его в
scoped_lock, получив двойной захват
Уточняющие вопросы
- →Чем
std::scoped_lockровно с одним мьютексом отличается отstd::lock_guard? - →Можно ли передать уже захваченные мьютексы через
std::adopt_lock?
MiddleТеорияЧастоКак работает спинлок? Когда он лучше мьютекса?
Как работает спинлок? Когда он лучше мьютекса?
Спинлок ждёт в цикле, не уступая CPU, и избегает накладных расходов syscall на сон/пробуждение мьютекса. Быстрее мьютекса только при очень короткой критической секции; на длинной — тратит CPU впустую.
Типичные ошибки
- ✗Использовать спинлок для критической секции, которая может занять более нескольких сотен наносекунд — сжигает CPU время
- ✗Реализовывать спинлок без инструкции
pause/yield— на x86_mm_pause()снижает энергопотребление и улучшает производительность HT - ✗Использовать спинлок в однопроцессорных средах — если владелец блокировки вытеснен, ожидающий крутится вечно
Уточняющие вопросы
- →Как
std::atomic_flagреализует минимальный спинлок? - →В чём разница между спинлоком и futex (fast userspace mutex)?
MiddleТеорияЧастоЧто происходит, если исключение покидает поток? Безопасные async-инструменты.
Что происходит, если исключение покидает поток? Безопасные async-инструменты.
Исключение, покидающее функцию std::thread, вызывает std::terminate. Для передачи между потоками используйте std::future/std::async (перебрасывается на get()) или std::exception_ptr для ручного захвата.
Типичные ошибки
- ✗Ловить исключения в потоке через
catch(...)и глотать их — молчаливый сбой, трудно отлаживать - ✗Не вызывать
future::get()— исключение, хранящееся в future, молча отбрасывается при его уничтожении - ✗Использовать
std::threadдля задач, которые могут бросать, без ручной передачи исключений — крэш вместо распространения
Уточняющие вопросы
- →Как передать несколько исключений из нескольких рабочих потоков обратно в главный поток?
- →Что такое
std::exception_ptrи безопасно ли его копировать через границы потоков?
MiddleКодЧастоНапишите потокобезопасный пул потоков.
Напишите потокобезопасный пул потоков.
Заранее создайте N рабочих, ожидающих общей очереди задач, защищённой мьютексом и condition variable. submit() кладёт std::function<void()> и будит один поток; деструктор ставит флаг остановки и делает join.
Типичные ошибки
- ✗Не перепроверять предикат после пробуждения
wait— ложные пробуждения доставляют поток с пустой очередью; всегда используйтеwait(lock, pred)или цикл - ✗Уничтожать пул при наличии задач в очереди — рабочие должны опустошить очередь перед выходом, или деструктор должен решить, отменять или завершить незавершённую работу
- ✗Блокировать
submit()при полной очереди без ограничения размера — неограниченные очереди могут исчерпать память под нагрузкой
Уточняющие вопросы
- →Как добавить поддержку приоритетов в очередь задач?
- →Как вернуть
std::futureизsubmit(), чтобы вызывающие могли ждать результатов?
SeniorКодЧастоРеализуйте producer-consumer с использованием condition variables.
Реализуйте producer-consumer с использованием condition variables.
Ограниченная очередь под мьютексом с двумя CV: not_full (производитель ждёт при полной) и not_empty (потребитель ждёт при пустой). Грейсфул через флаг done_ — потребители дренируют по done_ && queue_.empty().
Типичные ошибки
- ✗Использовать одну condition variable для 'не полный' и 'не пустой' — требует
notify_all()и тратит CPU; две CV сnotify_one()эффективнее - ✗Вызывать
notify_one()удерживая блокировку — технически корректно, но разбуженный поток сразу блокируется на мьютексе; рассмотрите уведомление после освобождения блокировки - ✗Не обрабатывать гонку при завершении — если
done_устанавливается до проверки потребителями, последние элементы могут быть потеряны; предикат должен бытьdone_ && queue_.empty(), а не простоdone_
Уточняющие вопросы
- →Как реализовать lock-free ограниченную очередь для producer-consumer?
- →Что такое back-pressure и как его реализовать, когда потребитель медленнее производителя?
SeniorТеорияЧастоЧто делает thread_local?
Что делает thread_local?
thread_local даёт каждому потоку собственную независимую копию переменной, инициализируемую при первом обращении и уничтожаемую при завершении потока.
Типичные ошибки
- ✗Использовать
thread_localдля состояния, которое должно быть привязано к задаче в пуле потоков — поток переиспользует состояние между задачами, что вызывает трудноуловимые баги - ✗Забывать, что статические члены класса
thread_localвсё равно должны быть определены ровно в одной единице трансляции (как любой статический член) - ✗Считать, что
thread_localобеспечивает синхронизацию — он устраняет разделение, но не гонки от одного потока, обращающегося к значению из нескольких корутин или обработчиков сигналов
Уточняющие вопросы
- →Как
thread_localвзаимодействует с динамическими библиотеками — разделяется ли переменная через границы DSO? - →Какова стоимость доступа к
thread_localна Linux (TLS через поиск смещения в регистре%fs)?
MiddleТеорияИногдаЧто такое std::atomic? Опции упорядочивания памяти.
Что такое std::atomic? Опции упорядочивания памяти.
std::atomic<T> делает RMW-операции неделимыми. Упорядочивания: relaxed, acquire/release, acq_rel и seq_cst (полный глобальный порядок, по умолчанию).
Типичные ошибки
- ✗Использовать
relaxedдля флага done/ready, сигнализирующего о готовности других данных — безrelease/acquireзаписи данных могут быть невидимы - ✗Считать
fetch_addсrelaxedнормальным для общего счётчика — для самого счётчика нормально, но не если нужна синхронизация на результате - ✗Использовать
atomic<std::string>— только тривиально копируемые типы lock-free;atomic<string>внутренне использует мьютекс
Уточняющие вопросы
- →Что такое последовательно согласованный полный порядок и почему он наиболее интуитивен, но наиболее дорог?
- →Как реализовать lock-free стек с использованием
compare_exchange_weak?
MiddleКодИногдаНапишите базовую реализацию std::atomic<T>.
Напишите базовую реализацию std::atomic<T>.
Минимальная Atomic<T> оборачивает значение и мьютекс с load/store/compare_exchange_strong. Настоящий std::atomic<T> для тривиально-копируемых типов использует lock-free инструкции CPU через intrinsic — без мьютекса.
Типичные ошибки
- ✗Забывать защищать
compare_exchange_strongатомарно — загрузка + условное сохранение с промежутком между ними не атомарны и разрушают смысл операции - ✗Использовать
volatileвместо атомиков для межпотоковой коммуникации —volatileпредотвращает переупорядочивание компилятором, но не CPU - ✗Опускать параметры memory_order — по умолчанию
memory_order_seq_cstкорректен, но самый дорогой; для простых флагов достаточноmemory_order_acquire/release
Уточняющие вопросы
- →Как CPU гарантирует атомарность 64-битного чтения на x86-64 без префикса LOCK?
- →Что такое проблема ABA и как
std::atomic<std::shared_ptr<T>>помогает?
MiddleТеорияИногдаМожет ли flock() синхронизировать потоки внутри одного процесса?
Может ли flock() синхронизировать потоки внутри одного процесса?
Нет. Блокировка flock() принадлежит описанию открытого файла, а не потоку. Все потоки одного процесса разделяют этот fd, поэтому видят одну и ту же блокировку, и flock() никогда не блокирует один поток против другого. Это рекомендательная блокировка между процессами; для потоков используйте std::mutex.
Типичные ошибки
- ✗Считать
flock()универсальной блокировкой и хвататься за неё для защиты общего состояния внутри процесса - ✗Считать
flock()обязательной — она рекомендательная: блокируются только сотрудничающие вызывающие, тоже вызывающиеflock() - ✗Думать, что блокировка принадлежит потоку или значению fd, а не описанию открытого файла
Уточняющие вопросы
- →Ведёт ли себя блокировка
fcntl()(POSIX record) иначе приfork()и в потоках? - →Как блокировка
flock()взаимодействует с дескриптором, продублированным черезdup()?
MiddleТеорияИногдаЧто такое livelock и чем он отличается от deadlock?
Что такое livelock и чем он отличается от deadlock?
Deadlock: потоки зависли в ожидании друг друга, прогресса нет. Livelock: потоки работают и меняют состояние в ответ друг другу, но полезной работы нет — обычно от наивной retry-логики без jitter.
Типичные ошибки
- ✗Крутить
while(!try_lock()) yield()из многих потоков — под нагрузкой livelock - ✗Считать высокую загрузку CPU «просто загруженностью» — может быть livelock
- ✗Добавлять backoff без рандомизации — N потоков всё равно синхронизируются на retry
Уточняющие вопросы
- →Что такое экспоненциальный backoff с jitter?
- →Как задача об обедающих философах показывает и deadlock, и livelock?
MiddleТеорияИногдаСколько потоков использовать для задачи? От чего это зависит?
Сколько потоков использовать для задачи? От чего это зависит?
CPU-bound задачи: один поток на логическое ядро (hardware_concurrency()); I/O-bound: больше потоков, чем ядер, так как они в основном ждут; закон Амдала ограничивает ускорение последовательной долей.
Типичные ошибки
- ✗Создавать один поток на запрос в сервере — может исчерпать лимиты ОС; используйте пул потоков
- ✗Устанавливать количество потоков равным
hardware_concurrency()для задач дискового I/O — слишком мало; диск-потоки большую часть времени ждут - ✗Не учитывать гиперпоточность —
hardware_concurrency()возвращает логические ядра (включая HT); для вычислительных задач физические ядра могут быть важнее
Уточняющие вопросы
- →Что такое закон Амдала и как он ограничивает максимальное ускорение параллельной программы?
- →Чем пулы потоков с похищением работы отличаются от пулов с фиксированным распределением?
SeniorТеорияИногдаЧто такое проблема ABA в lock-free коде и как с ней борются?
Что такое проблема ABA в lock-free коде и как с ней борются?
ABA: compare_exchange читает A, другой поток меняет его на B и обратно на A, поэтому CAS проходит, хотя структура под ним изменилась. Борются тегированным указателем (счётчик версии, растущий при каждом обновлении) или hazard pointers.
Типичные ошибки
- ✗Считать, что успешный CAS означает отсутствие изменений — он подтверждает лишь совпадение значения, а не истории
- ✗Сильнее всего ловить ABA на освобождённых и переаллоцированных узлах, которые аллокатор отдаёт по тому же адресу
- ✗Добавить счётчик версии, но дать ему переполниться, отчего ABA возвращается на достаточно длинном окне
Уточняющие вопросы
- →Как hazard pointers решают связанную задачу безопасного освобождения памяти?
- →Почему помогает двойной CAS (
DCAS) и какое железо его поддерживает?
SeniorТеорияИногдаЧто такое double-checked locking и почему он был сломан до C++11?
Что такое double-checked locking и почему он был сломан до C++11?
Double-checked locking читает ленивый указатель без lock, затем под lock проверяет повторно. До C++11 — UB: поток мог увидеть ненулевой указатель при неинициализированных полях.
Типичные ошибки
- ✗Делать double-checked locking с не-атомарным указателем в C++11+ — всё ещё UB
- ✗Считать, что
volatileобеспечивает синхронизацию в C++ - ✗Не использовать
std::call_onceиз-за «громоздкого» синтаксиса — это простейший корректный вариант
Уточняющие вопросы
- →Почему function-local static работает для lazy init?
- →Какова стоимость
std::call_onceпосле первого вызова?
SeniorПроизводительностьИногдаЧто такое false sharing и как его устранить?
Что такое false sharing и как его устранить?
False sharing — когда два потока пишут в разные переменные, попавшие на одну кэш-линию: каждая запись инвалидирует копию у другого ядра, вызывая ping-pong линии. Лечится выравниванием горячих данных на std::hardware_destructive_interference_size.
Типичные ошибки
- ✗Путать false sharing с настоящей гонкой данных — это баг производительности, не ошибка логики
- ✗Упаковывать счётчики потоков в плотный массив или структуру, так что соседние элементы делят кэш-линию
- ✗Хардкодить 64 как размер линии вместо
hardware_destructive_interference_size
Уточняющие вопросы
- →Как обнаружить false sharing с помощью
perfили VTune? - →Что такое true sharing и почему паддинг его не лечит?
SeniorТеорияИногдаЧто такое std::atomic_thread_fence и когда он нужен вместо атомарных операций?
Что такое std::atomic_thread_fence и когда он нужен вместо атомарных операций?
atomic_thread_fence(order) — самостоятельный барьер, упорядочивающий предыдущие и последующие операции в текущем потоке без атомарной загрузки или записи. Обычно проще задать release/acquire прямо на атомарной операции.
Типичные ошибки
- ✗Использовать
atomic_signal_fenceвместоatomic_thread_fence—signal_fenceупорядочивает только внутри потока (signal handler), не между потоками - ✗Считать, что fence сам по себе синхронизирует — нужна парная атомарная операция в другом потоке
- ✗Расставлять fence'ы вместо починки реальной гонки
Уточняющие вопросы
- →Сравните
atomic_thread_fence(release)сstore(release)— когда эквивалентны? - →Почему
atomic_signal_fenceдешевлеatomic_thread_fence?
SeniorТеорияИногдаОпишите lock-free структуры данных. Lock-free vs wait-free.
Опишите lock-free структуры данных. Lock-free vs wait-free.
Lock-free: хотя бы один поток всегда продвигается (дедлок невозможен, но отдельные потоки могут голодать). Wait-free: каждый поток завершает за ограниченное число шагов — самая сильная гарантия.
Типичные ошибки
- ✗Думать, что lock-free означает быстрее мьютекса — без конкуренции мьютекс часто быстрее; lock-free для высокой конкуренции или RT-ограничений
- ✗Не обрабатывать проблему ABA — приводит к повреждению структур данных, которые трудно воспроизвести
- ✗Использовать
compare_exchange_weakбез цикла повторной попытки — может ложно завершиться неудачей; всегда используйте цикл
Уточняющие вопросы
- →Как hazard pointers решают проблему безопасного освобождения памяти в lock-free структурах?
- →Что такое проблема ABA и как
std::atomic<std::shared_ptr<T>>(C++20) помогает?
SeniorТеорияИногдаЧто такое барьеры памяти и семантика acquire/release?
Что такое барьеры памяти и семантика acquire/release?
Барьер памяти запрещает переупорядочивание операций через него. Acquire (загрузка): последующие операции остаются после; release (сохранение): предыдущие — до. Пара release/acquire делает прежние записи писателя видимыми читателю.
Типичные ошибки
- ✗Использовать relaxed загрузки и сохранения для флага, охраняющего другие данные — окружающие не-атомарные записи могут быть переупорядочены за флаг
- ✗Думать, что барьеры нужны только на ARM — хотя x86 сильно упорядочен, компиляторное переупорядочивание всё равно требует C++ атомиков
- ✗Применять полный барьер памяти (
seq_cst) там, где нужен только acquire/release — корректно, но расточительно на ARM/Power
Уточняющие вопросы
- →В чём разница между барьером компилятора (
std::atomic_signal_fence) и аппаратным барьером памяти? - →Почему x86 не требует явных барьеров загрузки/сохранения, а ARM — требует?
SeniorТеорияИногдаЧто такое priority inversion и как с ним обычно борются?
Что такое priority inversion и как с ним обычно борются?
Высокоприоритетный поток ждёт lock у низкоприоритетного, а среднеприоритетные вытесняют держателя. Митигируется priority inheritance, priority ceiling или короткими критическими секциями.
Типичные ошибки
- ✗Назначать приоритеты потокам, не выбрав mutex с поддержкой priority inheritance
- ✗Длинные критические секции под контеншеном — даже без приоритетов латентность страдает
- ✗Смешивать realtime-приоритеты с обычными потоками на общих lock'ах
Уточняющие вопросы
- →Как работает
pthread_mutexattr_setprotocol(PTHREAD_PRIO_INHERIT)? - →Почему spinlock опасен при priority inversion?
SeniorДизайнИногдаВы проектируете C++-класс, чей публичный API будет вызываться из множества потоков одновременно, и он вызывает пользовательские колбэки. Дизайн должен делать гарантию потокобезопасности каждого метода однозначной для вызывающих, предотвращать гонки — например, когда вызывающий видит или действует на устаревшем состоянии между двумя вызовами — и избегать дедлока, когда колбэк повторно входит в объект. Опишите принципы, которыми вы руководствуетесь при проектировании такого API.
Вы проектируете C++-класс, чей публичный API будет вызываться из множества потоков одновременно, и он вызывает пользовательские колбэки. Дизайн должен делать гарантию потокобезопасности каждого метода однозначной для вызывающих, предотвращать гонки — например, когда вызывающий видит или действует на устаревшем состоянии между двумя вызовами — и избегать дедлока, когда колбэк повторно входит в объект. Опишите принципы, которыми вы руководствуетесь при проектировании такого API.
Минимизируйте разделяемое изменяемое состояние, явно документируйте гарантии потокобезопасности, инкапсулируйте RAII-блокировки на границах класса, не удерживайте блокировку при вызове колбэков и открывайте атомарные составные операции вместо отдельных check/act.
Типичные ошибки
- ✗Двухшаговые операции, открытые как два отдельных API-вызова — промежуток между ними создаёт гонку TOCTOU; вместо этого открывайте атомарные составные операции
- ✗Возвращать ссылки или итераторы во внутренние данные без удержания блокировки — вызывающий не может безопасно их использовать, не зная протокол блокировки
- ✗Рекурсивная блокировка с
std::mutexвместоstd::recursive_mutexкогда колбэки могут повторно входить в тот же класс — приводит к дедлоку
Уточняющие вопросы
- →Как модель акторов (например, Akka) обеспечивает thread-safety на архитектурном уровне?
- →Что такое паттерн 'монитор' и чем он отличается от произвольной блокировки?