Асинхронное программирование в C++: от std::async до корутин
Асинхронное программирование в C++ — это мощный инструмент для создания высокопроизводительных и отзывчивых приложений. В современном мире, где многоядерные процессоры стали стандартом, умение эффективно управлять параллельными задачами — обязательный навык для каждого C++ разработчика. В этой статье мы разберем ключевые концепции, начиная с базовых механизмов стандартной библиотеки и заканчивая современными корутинами из C++20.
Зачем нужно асинхронное программирование?
Основная цель асинхронного программирования — не блокировать основной поток выполнения программы. Представьте, что ваше приложение должно загрузить файл из сети, обработать его и показать пользователю. Если выполнять эти операции синхронно, интерфейс "зависнет" на время загрузки. Асинхронный подход позволяет запустить длительную операцию "в фоне", а основной поток продолжает обрабатывать пользовательский ввод.
Основные подходы к асинхронности в C++
В C++ существует несколько способов реализовать асинхронное выполнение:
- Ручное управление потоками (std::thread) — низкоуровневый, но гибкий подход.
- std::async и std::future — высокоуровневый механизм для запуска асинхронных задач.
- std::packaged_task и std::promise — для более тонкого контроля.
- Корутины (C++20) — современный и элегантный способ написания асинхронного кода.
Использование std::async и std::future
Это самый простой способ запустить асинхронную задачу. Функция std::async возвращает объект std::future, который будет содержать результат выполнения.
#include <iostream>
#include <future>
#include <thread>
#include <chrono>
int heavy_computation(int x) {
std::this_thread::sleep_for(std::chrono::seconds(2));
return x * x;
}
int main() {
std::cout << "Запускаем асинхронную задачу...\n";
std::future<int> result = std::async(std::launch::async, heavy_computation, 42);
// Делаем что-то ещё, пока задача выполняется
std::cout << "Основной поток не заблокирован!\n";
// Получаем результат (блокируется, если задача ещё не завершена)
int value = result.get();
std::cout << "Результат: " << value << "\n";
return 0;
}
Флаг std::launch::async гарантирует, что задача будет выполнена в отдельном потоке. Если его не указать, реализация может отложить выполнение до вызова .get().
Работа с std::promise и std::packaged_task
Эти механизмы предоставляют больше контроля. std::promise позволяет "обещать" значение, которое будет установлено позже, а std::packaged_task оборачивает вызываемый объект в задачу.
#include <iostream>
#include <future>
#include <thread>
void set_value(std::promise<int>&& prom) {
std::this_thread::sleep_for(std::chrono::seconds(1));
prom.set_value(100);
}
int main() {
std::promise<int> prom;
std::future<int> fut = prom.get_future();
std::thread t(set_value, std::move(prom));
std::cout << "Ожидаем результат...\n";
int value = fut.get();
std::cout << "Получено: " << value << "\n";
t.join();
return 0;
}
Важно: std::promise и std::future — это move-only объекты. Их нельзя копировать, только перемещать.
Корутины в C++20: новый уровень асинхронности
Корутины (coroutines) — это функции, которые могут приостанавливать своё выполнение и возобновлять его позже. Они позволяют писать асинхронный код так, как будто он синхронный.
Базовый пример корутины
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
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::terminate(); }
};
};
SimpleTask async_hello() {
std::cout << "Привет из корутины! Начинаем ожидание...\n";
co_await std::suspend_always{};
std::cout << "Продолжаем после приостановки!\n";
}
int main() {
async_hello();
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "Основной поток завершён.\n";
return 0;
}
Ключевое слово co_await приостанавливает выполнение корутины до тех пор, пока не будет выполнено некоторое условие (например, завершится ввод-вывод или пройдет таймер).
Практический пример: асинхронная загрузка данных
#include <iostream>
#include <future>
#include <coroutine>
#include <exception>
// Простая обёртка для std::future как awaitable
template<typename T>
struct FutureAwaiter {
std::future<T> future;
bool await_ready() { return future.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> handle) {
std::thread([this, handle]() {
future.wait();
handle.resume();
}).detach();
}
T await_resume() { return future.get(); }
};
template<typename T>
FutureAwaiter<T> make_awaitable(std::future<T> future) {
return {std::move(future)};
}
// Корутина
std::future<int> async_coroutine() {
auto result = co_await make_awaitable(std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return