Как хорошо ты знаешь многопоточность в C++?

«Как хорошо ты знаешь многопоточность в C++?» — вопрос из категории Многопоточность, который задают на 30% собеседований C/C++ Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Я работал с многопоточностью в C++ в нескольких проектах, где требовалась параллельная обработка данных и высокая производительность. Основной опыт включает:

1. Базовые механизмы потоков:

#include <iostream>
#include <thread>
#include <vector>

void process_data(int id, int& result) {
    // Имитация обработки
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    result = id * 10;
}

int main() {
    std::vector<std::thread> workers;
    std::vector<int> results(4);

    // Создание потоков
    for (int i = 0; i < 4; ++i) {
        workers.emplace_back(process_data, i, std::ref(results[i]));
    }

    // Ожидание завершения
    for (auto& t : workers) {
        t.join();
    }

    // Использование результатов
    for (int res : results) {
        std::cout << res << " ";
    }
    return 0;
}

2. Синхронизация с мьютексами:

#include <mutex>
#include <iostream>

class ThreadSafeCounter {
private:
    mutable std::mutex mtx;
    int value = 0;

public:
    void increment() {
        std::lock_guard<std::mutex> lock(mtx);
        ++value;
    }

    int get() const {
        std::lock_guard<std::mutex> lock(mtx);
        return value;
    }

    // C++17: безопасный захват нескольких мьютексов
    void transfer(ThreadSafeCounter& other, int amount) {
        std::scoped_lock lock(mtx, other.mtx);
        value -= amount;
        other.value += amount;
    }
};

3. Атомарные операции для счетчиков:

#include <atomic>
#include <thread>

std::atomic<int> atomic_counter{0};

void increment_atomic() {
    for (int i = 0; i < 1000; ++i) {
        atomic_counter.fetch_add(1, std::memory_order_relaxed);
    }
}

4. Условные переменные для producer-consumer:

#include <queue>
#include <condition_variable>

class MessageQueue {
    std::queue<std::string> messages;
    std::mutex mtx;
    std::condition_variable cv;
    bool stopped = false;

public:
    void push(const std::string& msg) {
        {
            std::lock_guard<std::mutex> lock(mtx);
            messages.push(msg);
        }
        cv.notify_one();
    }

    std::string pop() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this] { 
            return !messages.empty() || stopped; 
        });

        if (messages.empty()) return "";

        std::string msg = messages.front();
        messages.pop();
        return msg;
    }

    void stop() {
        {
            std::lock_guard<std::mutex> lock(mtx);
            stopped = true;
        }
        cv.notify_all();
    }
};

5. Продвинутые техники, с которыми я работал:

  • Thread pools с использованием std::async и std::future
  • Lock-free структуры данных для высоконагруженных систем
  • Memory ordering (memory_order_acquire, memory_order_release)
  • C++20 coroutines для асинхронных операций
  • Parallel algorithms из C++17 (std::for_each с execution policy)

Реальные проблемы, которые решал:

  • Гонки данных при доступе к общим кэшам
  • Взаимоблокировки при неправильном порядке захвата мьютексов
  • Производительность при contention на высоконагруженных мьютексах
  • Ложные пробуждения условных переменных

Лучшие практики, которым следую:

  1. Минимизация времени удержания блокировок
  2. Использование RAII (std::lock_guard, std::unique_lock)
  3. Предпочтение lock-free подходов там, где это возможно
  4. Тщательное тестирование на race conditions с помощью ThreadSanitizer
  5. Использование std::jthread (C++20) для автоматического управления жизненным циклом потоков