← Назад к вопросам

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

1.0 Junior🔥 171 комментариев
#Опыт работы и проекты#Многопоточность и синхронизация

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Многопоточность в C++

Многопоточность — это один из самых сложных и критических аспектов системного программирования. За 10+ лет работы я глубоко разбираюсь в этой области. Расскажу о ключевых концепциях и практике.

Основные примитивы синхронизации

std::mutex (взаимное исключение)

Базовый механизм для защиты критических секций:

std::mutex mtx;
int shared_counter = 0;

void increment() {
    std::lock_guard<std::mutex> lock(mtx);  // RAII
    shared_counter++;  // Безопасный доступ
}  // Автоматический unlock

std::lock_guard vs std::unique_lock

// lock_guard — простой, быстрый, RAII
std::lock_guard<std::mutex> lg(mtx);

// unique_lock — гибкий, с явным управлением
std::unique_lock<std::mutex> ul(mtx);
ul.unlock();  // Явный unlock
ul.lock();    // Явный lock

std::condition_variable (переменная условия)

Для синхронизации и сигнализации между потоками:

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

// Поток-производитель
void producer() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    {
        std::lock_guard<std::mutex> lg(mtx);
        ready = true;
    }
    cv.notify_one();  // Пробуди один ждущий поток
}

// Поток-потребитель
void consumer() {
    std::unique_lock<std::mutex> ul(mtx);
    cv.wait(ul, [] { return ready; });  // Спи пока ready == false
    std::cout << "Ready!\n";
}

Проблемы многопоточности

Race Condition (Гонка)

// ПЛОХО — race condition!
int counter = 0;

void increment_unsafe() {
    counter++;  // Не атомарно! 3 инструкции: load, add, store
}

// Если два потока выполняют одновременно:
// Thread 1: load (0) → add 1 → store (1)
// Thread 2: load (0) → add 1 → store (1)  ← потеря обновления!

Deadlock (Взаимная блокировка)

std::mutex mtx1, mtx2;

void thread1_func() {
    std::lock_guard<std::mutex> lg1(mtx1);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lg2(mtx2);  // Ждём mtx2
}

void thread2_func() {
    std::lock_guard<std::mutex> lg2(mtx2);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lg1(mtx1);  // Ждём mtx1
}  // DEADLOCK!

// Решение 1: всегда захватывай в одном порядке
// Решение 2: используй std::scoped_lock
std::scoped_lock sl(mtx1, mtx2);  // Избегает deadlock

Liveloss (Живой тупик)

Потоки активны, но не могут прогрессировать:

// Пример: spin-lock в цикле приводит к busy-waiting
while (!flag) {  // CPU работает впустую
    std::this_thread::yield();
}

Atomics (Атомарные операции)

#include <atomic>

std::atomic<int> counter(0);

void increment_safe() {
    counter++;  // Атомарная операция
}

void example() {
    // Явный контроль синхронизации
    counter.store(10, std::memory_order_release);
    int val = counter.load(std::memory_order_acquire);
    counter.compare_exchange_strong(val, 20);
}

Memory Order (Порядок памяти)

// memory_order_relaxed — только атомарность, без синхронизации
counter.store(10, std::memory_order_relaxed);

// memory_order_acquire/release — синхронизация между потоками
// memory_order_acq_rel — полная синхронизация
// memory_order_seq_cst — sequentially consistent (медленнее, но безопаснее)

Future и Promise

Для передачи результата из потока:

std::promise<int> promise;
std::future<int> future = promise.get_future();

std::thread t([&promise]() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    promise.set_value(42);
});

int result = future.get();  // Блокирует до получения результата
t.join();
std::cout << "Result: " << result << "\n";

async (Асинхронные операции)

std::future<int> result = std::async(std::launch::async, []() {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    return 42;
});

int value = result.get();  // Ждёт результата

Thread Pool (Пул потоков)

Практический пример для реальных проектов:

class ThreadPool {
private:
    std::vector<std::thread> workers;
    std::queue<std::function<void()>> tasks;
    std::mutex queue_mutex;
    std::condition_variable cv;
    bool stop = false;
    
public:
    ThreadPool(size_t num_threads) {
        for (size_t i = 0; i < num_threads; ++i) {
            workers.emplace_back([this] {
                while (true) {
                    std::unique_lock<std::mutex> lock(queue_mutex);
                    cv.wait(lock, [this] { return stop || !tasks.empty(); });
                    
                    if (stop && tasks.empty()) break;
                    
                    auto task = std::move(tasks.front());
                    tasks.pop();
                    lock.unlock();
                    
                    task();
                }
            });
        }
    }
    
    template<typename F>
    void enqueue(F&& f) {
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            tasks.push(std::forward<F>(f));
        }
        cv.notify_one();
    }
    
    ~ThreadPool() {
        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            stop = true;
        }
        cv.notify_all();
        for (auto& w : workers) w.join();
    }
};

Правила безопасного многопоточного кода

  1. Используй RAII — lock_guard, unique_lock
  2. Минимизируй критические секции — меньше блокировок = лучше производительность
  3. Избегай вложенных блокировок — источник deadlock
  4. Дизайн без блокировок (lock-free) — где возможно используй atomics
  5. Правильное использование condition_variable — всегда проверяй предикат в цикле
// ПЛОХО
cv.wait(lock);

// ХОРОШО
cv.wait(lock, [this] { return predicate(); });

Инструменты отладки

  • ThreadSanitizer (tsan) — обнаруживает race conditions
  • Helgrind (valgrind) — анализ многопоточных проблем
  • Profilers — perf, gperftools для анализа производительности
g++ -fsanitize=thread -g -o program program.cpp
./program  # Обнаружит race conditions

Современный подход (C++17+)

// scoped_lock — удобнее
std::scoped_lock lg(mtx1, mtx2);  // Захватывает оба без deadlock

// structured binding
auto [value, success] = some_concurrent_operation();

// ranges и parallel algorithms
std::ranges::for_each(
    std::execution::par,  // Параллельное выполнение
    vec, transform_func
);

Вывод

Многопоточность — это сложная область, требующая глубокого понимания:

  • Memory models и синхронизация
  • Primitives (mutex, condition_variable, atomic)
  • Patterns (пулы потоков, producer-consumer)
  • Problems (deadlock, race conditions, livelock)
  • Tools для отладки и профилирования

Я использую многопоточность в production коде на протяжении 10+ лет, применяя лучшие практики и избегая типичных ошибок. Регулярно пишу thread-safe системы, пулы потоков, и детально разбираюсь в оптимизации synchronization для максимальной производительности.