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

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

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

Начать курс

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

Современные приложения всё чаще требуют одновременного выполнения нескольких задач. Будь то обработка пользовательских запросов на сервере, рендеринг графики или работа с большими данными — конкурентность и многопоточность в C++ стали обязательными навыками разработчика. В этой статье мы разберём ключевые механизмы, доступные в стандартной библиотеке C++, начиная с C++11 и заканчивая C++20. Вы узнаете, как создавать потоки, синхронизировать их работу и избегать типичных ошибок.

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

1. Основы многопоточности: std::thread и управление потоками

В C++11 появилась стандартная поддержка потоков через класс 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() для каждого созданного потока, иначе при разрушении объекта std::thread программа аварийно завершится. join() блокирует текущий поток до завершения дочернего, а detach() позволяет потоку работать независимо.

Для передачи аргументов в поток используйте дополнительные параметры конструктора:

#include <thread>
#include <string>

void print_sum(int a, int b, const std::string& msg) {
    std::cout << msg << ": " << (a + b) << std::endl;
}

int main() {
    std::thread t(print_sum, 3, 4, "Сумма");
    t.join();
    return 0;
}

Обратите внимание: аргументы копируются внутрь потока. Если нужно передать ссылку, используйте std::ref().

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

При работе с общими данными из разных потоков возникает гонка данных (data race). Чтобы её избежать, используется синхронизация. Основной инструмент — std::mutex.

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

std::mutex mtx;
int shared_counter = 0;

void increment() {
    for (int i = 0; i < 100000; ++i) {
        mtx.lock();
        ++shared_counter;
        mtx.unlock();
    }
}

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

Однако ручной вызов lock() и unlock() чреват ошибками: если между ними произойдёт исключение, мьютекс останется заблокированным. Лучше использовать RAII-обёртку std::lock_guard или std::unique_lock:

void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++shared_counter;
    }  // автоматически разблокируется при выходе из области видимости
}

Для более гибкого управления (например, условная блокировка) используйте std::unique_lock. А если нужно синхронизировать несколько мьютексов без взаимоблокировки, применяйте std::lock().

3. Асинхронные задачи: std::async и std::future

Не всегда удобно управлять потоками вручную. Для запуска асинхронных задач и получения результата в C++ есть std::async и std::future. Они позволяют выполнить функцию в отдельном потоке (или отложить выполнение) и получить результат через объект future.

#include <iostream>
#include <future>

int compute(int x) {
    return x * x;
}

int main() {
    std::future<int> result = std::async(std::launch::async, compute, 5);
    std::cout << "Квадрат 5 = " << result.get() << std::endl;
    return 0;
}

Политика std::launch::async гарантирует выполнение в новом потоке. std::launch::deferred откладывает вызов до вызова get() — выполнение происходит в том же потоке. Комбинация std::launch::async | std::launch::deferred (по умолчанию) позволяет библиотеке решать сама.

Метод get() блокирует текущий поток до получения результата. Если задача выбросит исключение, оно будет проброшено в месте вызова get(). Это удобно для обработки ошибок.

4. Атомарные операции и модель памяти C++

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

#include <iostream>
#include <atomic>
#include <thread>

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

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

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Счётчик: " << counter.load() << std::                

Блоги

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