Конкурентность и многопоточность в C++: полное руководство

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в конкурентность и многопоточность в C++

Современные приложения требуют максимальной производительности. Конкурентность и многопоточность в C++ позволяют эффективно использовать многоядерные процессоры, ускоряя выполнение задач. Без понимания этих концепций невозможно создавать высоконагруженные системы, обработчики данных или игровые движки.

В этой статье мы разберем ключевые механизмы многопоточности в C++, начиная с базовых std::thread и заканчивая продвинутыми паттернами. Вы узнаете, как избежать гонок данных, правильно синхронизировать потоки и писать безопасный параллельный код.

Основы многопоточности: std::thread и std::jthread

Класс std::thread (C++11) — фундамент многопоточного программирования. Он позволяет запустить функцию или callable-объект в отдельном потоке. Рассмотрим простейший пример:

#include <iostream>
#include <thread>

void worker(int id) {
    std::cout << "Поток " << id << " начал работу\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Поток " << id << " завершен\n";
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    
    t1.join();
    t2.join();
    return 0;
}

Ключевые моменты:

  • join() — основной поток ждет завершения дочернего потока
  • detach() — поток продолжает работу независимо (риск утечки ресурсов)
  • std::jthread (C++20) — автоматически вызывает join() в деструкторе

Всегда проверяйте joinable() перед вызовом join(), чтобы избежать двойного присоединения.

Синхронизация потоков: мьютексы и блокировки

Главная проблема многопоточности — гонки данных, когда несколько потоков одновременно изменяют общую переменную. Для защиты используются мьютексы.

std::mutex и std::lock_guard

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Результат: " << counter << "\n"; // 200000
    return 0;
}

std::lock_guard автоматически освобождает мьютекс при выходе из области видимости. Для более гибкого управления используйте std::unique_lock.

Deadlock и std::scoped_lock

Взаимная блокировка (deadlock) возникает, когда два потока ждут друг друга. Решение — использовать std::scoped_lock (C++17), который захватывает несколько мьютексов атомарно:

std::mutex m1, m2;

void safe_function() {
    std::scoped_lock lock(m1, m2);
    // безопасная работа с двумя ресурсами
}

Параллельные алгоритмы и std::async

Для выполнения асинхронных задач без ручного управления потоками используйте std::async. Он возвращает std::future, из которого можно получить результат.

#include <iostream>
#include <future>
#include <chrono>

int compute(int n) {
    std::this_thread::sleep_for(std::chrono::seconds(n));
    return n * n;
}

int main() {
    auto future1 = std::async(std::launch::async, compute, 2);
    auto future2 = std::async(std::launch::async, compute, 3);
    
    std::cout << "Результаты: " << future1.get() 
              << ", " << future2.get() << "\n"; // 4, 9
    return 0;
}

Политики запуска:

  • std::launch::async — запуск в новом потоке
  • std::launch::deferred — ленивое выполнение при вызове get()

Параллельные алгоритмы STL (C++17)

Многие алгоритмы STL получили параллельные версии. Укажите политику выполнения std::execution::par для автоматической параллелизации:

#include <algorithm>
#include <execution>
#include <vector>

std::vector<int> data(1000000);
std::iota(data.begin(), data.end(), 0);

// Параллельная сортировка
std::sort(std::execution::par, data.begin(), data.end());

Атомарные операции и барьеры памяти

Для простых операций (инкремент, флаги) мьютексы избыточны. Используйте std::atomic — это обеспечивает атомарность без блокировок.

#include <atomic>

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

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

int main() {
    std::thread t1(atomic_increment);
    std::thread t2(atomic_increment);
    t1.join();
    t2.join();
    std::cout << atomic_counter.load() << "\n"; // 200000
    return 0;
}

Порядки памяти (memory order):

  • memory_order_relaxed — минимальные гарантии, максимальная скорость
  • memory_order_acquire/release — для синхронизации между потоками
  • memory_order_seq_cst — самый строгий (по умолчанию)

Барьеры памяти и std::atomic_thread_fence

Иногда нужно синхронизировать не атомарные переменные. Для этого существуют барьеры памяти:

std::atomic<bool> flag{false};
int shared_data = 0;

void writer() {                

Блоги

Рекомендаций по книгам