Асинхронное программирование в C++: полное руководство
Современные приложения требуют высокой производительности и отзывчивости. Асинхронное программирование в C++ позволяет выполнять длительные операции (ввод-вывод, сетевые запросы, сложные вычисления) без блокировки основного потока. В этом руководстве мы разберем ключевые механизмы C++ для асинхронной работы: от классических std::thread и std::async до современных корутин C++20. Вы узнаете, как писать эффективный и безопасный асинхронный код.
1. Основы: std::thread и std::async
Начнем с базовых инструментов. std::thread создает новый поток выполнения, но требует ручного управления и синхронизации. Более удобный вариант — std::async, который запускает функцию асинхронно и возвращает std::future для получения результата.
#include <iostream>
#include <future>
#include <chrono>
int long_running_task(int input) {
std::this_thread::sleep_for(std::chrono::seconds(2));
return input * 10;
}
int main() {
std::cout << "Запускаем асинхронную задачу...\n";
std::future<int> result = std::async(std::launch::async, long_running_task, 5);
// Делаем что-то полезное, пока задача выполняется
std::cout << "Основной поток продолжает работу...\n";
// Получаем результат (блокируется, если задача еще не завершена)
int value = result.get();
std::cout << "Результат: " << value << std::endl;
return 0;
}
Ключевые моменты:
- std::launch::async — гарантирует запуск в отдельном потоке.
- std::launch::deferred — откладывает выполнение до вызова
get(). - Метод
get()блокирует поток до получения результата.
2. Продвинутая работа: std::promise и std::future
Когда нужно передать результат из одного потока в другой, используйте пару std::promise и std::future. promise устанавливает значение, а future его получает. Это особенно полезно при пулинге потоков или цепочках задач.
#include <iostream>
#include <future>
#include <thread>
void worker(std::promise<int> prom) {
try {
int result = 42; // Сложные вычисления
prom.set_value(result);
} catch (...) {
prom.set_exception(std::current_exception());
}
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(worker, std::move(prom));
// Получаем результат
int value = fut.get();
std::cout << "Получено значение: " << value << std::endl;
t.join();
return 0;
}
Важные особенности:
promiseнельзя копировать, только перемещать.- Исключения передаются через
set_exception. futureможно использовать только один раз (однократное получение).
3. Корутины C++20: современный подход
Корутины (coroutines) — мощный инструмент, добавленный в C++20. Они позволяют приостанавливать и возобновлять выполнение функции, не блокируя поток. Это идеально для асинхронных операций ввода-вывода и генераторов.
#include <iostream>
#include <coroutine>
#include <future>
struct SimpleTask {
struct promise_type {
SimpleTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::exit(1); }
};
};
SimpleTask async_operation() {
std::cout << "Начало корутины\n";
co_await std::suspend_always{};
std::cout << "Продолжение после приостановки\n";
}
int main() {
auto task = async_operation();
std::cout << "Основной поток\n";
return 0;
}
Более практичный пример с использованием std::future:
#include <iostream>
#include <coroutine>
#include <future>
#include <thread>
class AsyncFuture {
public:
struct promise_type {
AsyncFuture get_return_object() { return AsyncFuture{this}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
AsyncFuture(promise_type* p) : promise_(p) {}
~AsyncFuture() { if (handle_) handle_.destroy(); }
private:
std::coroutine_handle<promise_type> handle_;
};
AsyncFuture async_task() {
std::cout << "Корутина: начало\n";
co_await std::suspend_always{};
std::cout << "Корутина: продолжение\n";
}
int main() {
auto task = async_task();
std::cout << "Главный поток\n";
return 0;
}
Корутины позволяют писать асинхронный код, который выглядит как синхронный. Однако они требуют создания собственных типов-оберток (promise_type), что может быть сложно для новичков.
4. Пул потоков и параллельные алгоритмы
Для массовых параллельных вычислений используйте std::execution (C++17) и пулы потоков. Это позволяет легко распараллелить алгоритмы STL.
#include <iostream>
#include <vector>
#include <algorithm>
#include <execution>
#include <numeric>
int main() {
std::vector<int> data(1000000);
std::iota(data.begin(), data.end(), 1);
// Параллельное суммирование
long long sum = std::reduce(std::execution::par, data.begin(), data.end(), 0LL);
std::cout << "Сумма: " << sum << std::endl;
// Параллельная сортировка
std::sort(std::execution::par, data.begin(), data