Введение в асинхронное программирование на C++
В современном мире разработки программного обеспечения производительность и отзывчивость приложений играют ключевую роль. Асинхронное программирование в C++ позволяет эффективно использовать ресурсы системы, выполняя несколько задач одновременно без блокировки основного потока. Это особенно важно при работе с сетевыми запросами, операциями ввода-вывода и длительными вычислениями.
В отличие от традиционного синхронного подхода, где каждая операция выполняется последовательно, асинхронный код может запускать задачи в фоновом режиме и получать результаты по мере их готовности. В этом руководстве мы рассмотрим основные инструменты асинхронного программирования в C++, начиная с простых концепций и заканчивая современными возможностями стандарта C++20.
Основы асинхронности: std::async и std::future
Самый простой способ начать работу с асинхронным программированием в C++ — использовать std::async. Эта функция запускает задачу асинхронно и возвращает объект std::future, который содержит результат выполнения.
Базовый пример с std::async
#include <iostream>
#include <future>
#include <chrono>
int длительная_операция() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
}
int main() {
std::cout << "Запускаем асинхронную задачу..." << std::endl;
// Запуск асинхронной задачи
std::future<int> результат = std::async(std::launch::async, длительная_операция);
std::cout << "Основной поток продолжает работу..." << std::endl;
// Получение результата (блокирует поток до готовности)
int значение = результат.get();
std::cout << "Результат: " << значение << std::endl;
return 0;
}
Политика запуска std::launch::async гарантирует, что функция будет выполнена в отдельном потоке. Альтернатива — std::launch::deferred, которая откладывает выполнение до момента вызова get().
Работа с несколькими задачами
Асинхронное программирование особенно эффективно при параллельном выполнении нескольких независимых задач:
#include <iostream>
#include <future>
#include <vector>
int вычислить_квадрат(int x) {
return x * x;
}
int main() {
std::vector<std::future<int>> задачи;
// Запускаем несколько асинхронных задач
for (int i = 1; i <= 5; ++i) {
задачи.push_back(std::async(std::launch::async, вычислить_квадрат, i));
}
// Собираем результаты
for (auto& задача : задачи) {
std::cout << "Результат: " << задача.get() << std::endl;
}
return 0;
}
Продвинутые инструменты: std::promise и std::packaged_task
Для более гибкого управления асинхронными операциями в C++ предусмотрены std::promise и std::packaged_task. Эти инструменты позволяют передавать значения между потоками и оборачивать вызываемые объекты.
Использование std::promise
std::promise позволяет установить значение, которое впоследствии будет получено через соответствующий объект std::future:
#include <iostream>
#include <future>
#include <thread>
void поток_производитель(std::promise<std::string> обещание) {
try {
std::string данные = "Важные данные из потока";
// Устанавливаем значение
обещание.set_value(данные);
} catch(...) {
// Обработка исключений
обещание.set_exception(std::current_exception());
}
}
int main() {
std::promise<std::string> обещание;
std::future<std::string> будущее = обещание.get_future();
std::thread поток(поток_производитель, std::move(обещание));
// Получаем результат (блокируется до готовности)
std::cout << "Получено: " << будущее.get() << std::endl;
поток.join();
return 0;
}
std::packaged_task для обертки функций
std::packaged_task оборачивает любую вызываемую сущность и автоматически создает std::future для получения результата:
#include <iostream>
#include <future>
#include <thread>
int умножить(int a, int b) {
return a * b;
}
int main() {
// Оборачиваем функцию
std::packaged_task<int(int, int)> задача(умножить);
std::future<int> результат = задача.get_future();
// Запускаем в отдельном потоке
std::thread поток(std::move(задача), 7, 8);
std::cout << "7 * 8 = " << результат.get() << std::endl;
поток.join();
return 0;
}
Современный подход: корутины C++20
Стандарт C++20 привнес корутины — мощный инструмент для написания асинхронного кода, который выглядит как синхронный. Корутины позволяют приостанавливать и возобновлять выполнение функции без блокировки потока.
Базовая структура корутины
#include <iostream>
#include <coroutine>
#include <thread>
struct Генератор {
struct promise_type {
int текущее_значение;
Генератор get_return_object() {
return Генератор{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(int значение) {
текущее_значение = значение;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
int получить_значение() {
return handle.promise().текущее_значение;
}
bool продолжить() {
if (!handle.done()) {
handle.resume();
return true;
}
return false;
}