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

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

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

Начать курс

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

Современные процессоры имеют множество ядер, и чтобы использовать их полную мощность, программы должны уметь выполнять несколько задач одновременно. Конкурентность (concurrency) — это способность программы выполнять несколько задач в перекрывающиеся промежутки времени, а многопоточность (multithreading) — один из способов её реализации, при котором программа делится на несколько потоков, работающих параллельно.

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

Основы: создание и управление потоками

std::thread — базовый строительный блок

Класс std::thread позволяет запустить функцию или вызываемый объект в отдельном потоке. Рассмотрим простейший пример:

#include <iostream>
#include <thread>

void hello() {
    std::cout << "Привет из потока " 
              << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t(hello);   // Запускаем поток
    t.join();               // Ждём завершения потока
    return 0;
}

Метод join() заставляет основной поток ожидать завершения дочернего. Альтернатива — detach(), который позволяет потоку работать независимо, но требует особой осторожности (поток может пережить создавшие его объекты).

Передача аргументов и лямбда-функции

Потокам можно передавать аргументы и использовать лямбды:

#include <thread>
#include <string>

void process(int id, const std::string& data) {
    // Обработка данных
}

int main() {
    std::thread t1(process, 1, "данные");
    std::thread t2([](int x) {
        // Лямбда-выражение
    }, 42);
    
    t1.join();
    t2.join();
}

Синхронизация потоков: защита общих данных

Когда несколько потоков обращаются к одним и тем же данным, возникает состояние гонки (race condition). Для его предотвращения используются механизмы синхронизации.

Мьютексы (std::mutex)

Мьютекс — это примитив синхронизации, который гарантирует, что только один поток может владеть им в любой момент времени:

#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: " << counter << std::endl;
    return 0;
}

std::lock_guard — это RAII-обёртка, которая автоматически захватывает мьютекс при создании и освобождает при разрушении. Это защищает от случайного забывания отпустить мьютекс.

Условные переменные (std::condition_variable)

Условные переменные позволяют потокам ждать наступления определённого события:

#include <queue>
#include <condition_variable>

std::queue<int> data_queue;
std::mutex queue_mutex;
std::condition_variable cv;

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
        std::lock_guard<std::mutex> lock(queue_mutex);
        data_queue.push(i);
        cv.notify_one();  // Уведомляем потребителя
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(queue_mutex);
        cv.wait(lock, []{ return !data_queue.empty(); });
        
        int value = data_queue.front();
        data_queue.pop();
        lock.unlock();
        
        // Обрабатываем value
        if (value == 9) break;
    }
}

Продвинутые техники работы с конкурентностью

std::async и std::future

Более высокоуровневый подход — использование std::async, который запускает задачу асинхронно и возвращает std::future для получения результата:

#include <future>
#include <iostream>

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

int main() {
    std::future<int> result = std::async(std::launch::async, compute, 5);
    
    std::cout << "Ожидаем результат..." << std::endl;
    int value = result.get();  // Блокируется до получения результата
    std::cout << "Результат: " << value << std::endl;
    
    return 0;
}

Флаг std::launch::async гарантирует выполнение в отдельном потоке, а std::launch::deferred откладывает выполнение до вызова get().

Атомарные операции (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);
    }
}

// Использование аналогично предыдущему примеру с мьютексом

Параметр std::memory_order определяет гарантии упорядочивания операций. memory_order_relaxed — самый быстрый, но не гарантирует порядка видимости. Для большинства случаев подходит memory_order_seq_cst (по умолчанию).

Ошибки и best practices

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

Deadlock возникает, когда два или более потоков ждут освобождения ресурсов, которыми владеют друг друга:

// Потенциальный deadlock
void thread1() {
    std::lock_guard<std::mutex> lock1(mtx1);
    std::this_thread::sleep_for(std::chrono::milliseconds(100));                

Блоги

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