Форум программистов, компьютерный форум, киберфорум
NullReferenced
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

Как std::execution меняет асинхронное программировани­­­е в C++26

Запись от NullReferenced размещена 08.03.2025 в 21:34
Показов 2986 Комментарии 1

Нажмите на изображение для увеличения
Название: 7acbed55-e45e-4028-8b55-27dc0b0c5dbe.jpg
Просмотров: 545
Размер:	277.2 Кб
ID:	10340
Асинхронное программирование долго было одной из самых сложных сторон C++. Мы были вынуждены прибегать к различным библиотекам и способам: от стандартных потоков и future/promise до сторонних решений вроде Boost.Asio или даже полностью самописных систем. C++11, C++14 и C++17 постепенно улучшали ситуацию, но недостаточно. C++26 может это изменить с помощью нового компонента std::execution. Этот фреймворк представляет собой не просто новый API, а полноценную систему для асинхронного и параллельного выполнения кода.

Предложение P2300R10, на котором основан std::execution, было разработано с учетом множества реальных сценариев использования. Оно предлагает несколько ключевых положений:
  • Композируемость и универсальность, позволяющие писать код, который может работать с различными типами исполнителей.
  • Инкапсуляция распространенных асинхронных шаблонов в настраиваемые и многоразовые алгоритмы.
  • Возможность быть "корректным по построению", минимизируя распространенные ошибки.
  • Поддержка разнообразных исполнителей и ресурсов выполнения.

В центре архитектуры std::execution находятся понятия "sender" (отправитель), "receiver" (получатель) и "scheduler" (планировщик). Такая модель позволяет абстрагироваться от конкретных механизмов многопоточности и сосредоточиться на самой логике асинхронных вычислений. Особенно важно, что std::execution предоставляет встроенный метод составления асинхронных операций в цепочки, что делает код более читаемым и менее подверженным ошибкам, чем традиционные подходы с обратными вызовами или вложенными блоками.

Технические основы



Модель sender/receiver, на которой построен std::execution меняет подход к асинхронному программированию в C++. В отличие от традиционных абстракций вроде потоков или задач, она разделяет асинхронные операции на два чётко определённых компонента:

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

Receiver (получатель) — это объект, который обрабатывает результат выполнения сендера. Он имеет три ключевых метода: set_value (вызывается при успешном завершении), set_error (вызывается при ошибке) и set_stopped (вызывается при отмене операции).

Взаимодействие между ними происходит через connect, который связывает сендер и ресивер, создавая операцию, которую можно запустить вызовом start:

C++
1
2
auto operation = stdexec::connect(sender, receiver);
stdexec::start(operation);
В реальности, редко приходится работать напрямую с ресиверами. Вместо этого обычно используют стандартные функции, которые неявно создают и соединяют ресиверы с сендерами:

C++
1
2
3
4
5
6
7
8
9
10
11
exec::static_thread_pool pool(8);
auto sch = pool.get_scheduler();
 
auto begin = stdexec::schedule(sch);
auto hi = stdexec::then(begin, [] {
    std::cout << "Hello world! Have an int.\n";
    return 13;
});
auto add_42 = stdexec::then(hi, [](int arg) { return arg + 42; });
 
auto [i] = stdexec::sync_wait(add_42).value();
В этом примере schedule создаёт сендер, then преобразует сендер, а sync_wait запускает выполнение цепочки сендеров и ожидает результат.

Важной частью архитектуры является scheduler (планировщик). Это абстракция ресурса выполнения, например пул потоков, основной поток или специализированное оборудование, предоставляющая унифицированный интерфейс для планирования работы. Scheduler отвечает на вопрос "где выполнять код?", а не "что выполнять".

Сравнивая с существующими моделями асинхронности, можно выделить следующие отличия:

1. В отличие от std::future/promise, модель sender/receiver:
- Не создаёт неявных потоков или состояний
- Поддерживает отмену операций
- Позволяет явно указать, где будет выполняться код
- Обеспечивает более эффективную композицию операций

2. По сравнению с callback-подходами:
- Избегает глубокой вложенности (callback hell)
- Обеспечивает более явную обработку ошибок
- Позволяет алгоритмически трансформировать операции

3. По сравнению с корутинами:
- Работает без поддержки компилятора
- Может быть более эффективной по производительности в некоторых случаях
- Легче интегрируется с существующими алгоритмами STL

Однако std::execution отлично совместим с корутинами C++20, и во многих случаях их комбинирование даёт наилучшие результаты.
Фреймворк предлагает несколько важных классов сендеров:
Фабрики сендеров (sender factories): schedule, just, just_error, just_stopped, read_env
Адаптеры сендеров (sender adaptors): then, let_value, let_error, let_stopped, upon_error, upon_stopped, bulk, split и др.

С этими компонентами вы можете выразить практически любые асинхронные алгоритмы компактно и без лишней сложности.

Не могу разобраться как обновить в std::map<std::string, вектор_структур>
Не могу разобраться как обновить вектор структур после его добавления в map без удаления и перезаписи struct pStruct { int a; ...

std::weak_ptr & std::enable_shared_for_this. Как передаем this?
#include &lt;iostream&gt; #include &lt;memory&gt; class SharedObject : public std::enable_shared_from_this&lt;SharedObject&gt; { public: int x = 1; ...

Как устанавливат­ь QT offline или online
Всем доброго времени суток! Столкнулся с необходимостью установки QT и вытекающими из этого процессаа проблемами, надеюсь на ваше участие: - онлайн...

std::pair<std::list<std::pair< >>::iterator, > ломается при возврате из функции
#include &lt;iostream&gt; #include &lt;list&gt; #include &lt;string&gt; #include &lt;utility&gt; using lp = std::list&lt;std::pair&lt;std::string, int&gt;&gt;; auto f(lp...


Практическое применение



Теоретические основы std::execution производят впечатление, но давайте попробуем его на практике. Начнем с простейшего примера — последовательного выполнения нескольких операций:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Создаем пул потоков и получаем планировщик
exec::static_thread_pool pool(4);
auto sch = pool.get_scheduler();
 
// Формируем цепочку операций
auto sender = stdexec::schedule(sch)
    | stdexec::then([](){ return "обработка данных"; })
    | stdexec::then([](std::string s){ 
        std::cout << "Начинаем " << s << "\n"; 
        return 42; 
    });
 
// Запускаем выполнение и получаем результат
auto [result] = stdexec::sync_wait(sender).value();
Обратите внимание на оператор конвейера |, который делает композицию сендеров более красивой, чем вложенные вызовы функций. Это особенно важно при построении сложных цепочек.
Для параллельного выполнения независимых операций используется адаптер when_all:

C++
1
2
3
4
5
6
7
auto parallel_sender = stdexec::when_all(
    stdexec::schedule(sch) | stdexec::then([]{ return compute_part1(); }),
    stdexec::schedule(sch) | stdexec::then([]{ return compute_part2(); }),
    stdexec::schedule(sch) | stdexec::then([]{ return compute_part3(); })
) | stdexec::then([](int r1, int r2, int r3) {
    return r1 + r2 + r3;
});
Здесь три операции запускаются параллельно и их результаты объединяются после завершения всех трёх.
Для обработки массивов данных std::execution предлагает bulk, который применяет функцию к каждому элементу диапазона:

C++
1
2
3
4
5
6
7
8
9
std::vector<int> data(1000);
// Заполняем вектор...
 
auto bulk_sender = stdexec::schedule(sch)
    | stdexec::bulk(data.size(), [&](std::size_t i) {
        data[i] = process(data[i]);
    });
 
stdexec::sync_wait(bulk_sender);
Этот код обрабатывает элементы вектора параллельно, распределяя работу между потоками пула.
Одним из важных моментов асинхронного программирования является правильная обработка ошибок. В std::execution эта задача решается с помощью адаптеров upon_error и let_error:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
auto error_handling_sender = stdexec::schedule(sch)
 | stdexec::then([]{ 
     // Представим, что здесь может возникнуть исключение
     if (std::rand() % 10 == 0)
         throw std::runtime_error("Произошла ошибка!");
     return "Операция успешна";
 })
 | stdexec::upon_error([](std::exception_ptr eptr) {
     try {
         std::rethrow_exception(eptr);
     } catch (const std::exception& e) {
         std::cerr << "Перехвачена ошибка: " << e.what() << "\n";
     }
 });
Адаптер upon_error выполняет функцию только если предыдущая операция завершилась с ошибкой. Если нужно вернуться в "счастливый путь" после обработки ошибки, используется let_error.
Иногда нужно переключаться между различными контекстами выполнения. Например, начать работу в фоновом потоке, а завершить в главном потоке для обновления UI:

C++
1
2
3
4
5
6
7
8
// Планировщик для главного потока
auto main_scheduler = /* ... */;
 
auto ui_update_sender = stdexec::schedule(sch)
 | stdexec::then([]{ return calculate_complex_data(); })
 | stdexec::then(continues_on(main_scheduler), [](auto data) {
     update_user_interface(data);
 });
Адаптер continues_on указывает, что следующая операция должна выполняться в контексте указанного планировщика.
Для более сложных сценариев когда результат одной операции определяет, какие операции выполнять далее, используется let_value:

C++
1
2
3
4
5
6
7
8
auto dynamic_flow = stdexec::schedule(sch)
 | stdexec::then([]{ return check_condition(); })
 | stdexec::let_value([&](bool condition) {
     if (condition)
         return path_a();
     else
         return path_b();
 });
let_value позволяет динамически выбирать следующий сендер на основе результата предыдущей операции.
Преобразование обычных алгоритмов в асинхронные становится более понятным. Например, параллельная редукция:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
template <typename T, typename BinaryOp>
auto parallel_reduce(std::span<T> data, T init, BinaryOp op, exec::scheduler auto sch) {
    const size_t chunk_size = std::max(size_t(1), data.size() / 16);
    std::vector<stdexec::sender auto> chunk_senders;
    
    for (size_t i = 0; i < data.size(); i += chunk_size) {
        size_t end = std::min(i + chunk_size, data.size());
        chunk_senders.push_back(
            stdexec::schedule(sch) 
            | stdexec::then([=, &data, &op]() {
                T result = init;
                for (size_t j = i; j < end; ++j) {
                    result = op(result, data[j]);
                }
                return result;
            })
        );
    }
    
    return stdexec::when_all_vector(std::move(chunk_senders))
        | stdexec::then([=](std::vector<T> results) {
            T final_result = init;
            for (const T& r : results) {
                final_result = op(final_result, r);
            }
            return final_result;
        });
}
Этот алгоритм разбивает данные на куски, обрабатывает их параллельно, а затем объединяет результаты.

Производительность и оптимизация



Архитектурные решения, лежащие в основе std::execution, нацелены не только на удобство использования, но и на максимальную производительность. Можно сказать, одним из главных преимуществ модели sender/receiver является оптимизация на этапе компиляции, что существенно снижает накладные расходы во время выполнения. При сравнении с традиционными подходами асинхронного программирования в C++, std::execution демонстрирует действительно впечатляющие результаты. Рассмотрим сценарий параллельной обработки данных:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Традиционный подход с std::async
std::vector<std::future<int>> futures;
for (int i = 0; i < data.size(); ++i) {
    futures.push_back(std::async(std::launch::async, [&data, i]() {
        return process_item(data[i]);
    }));
}
int sum = 0;
for (auto& f : futures) {
    sum += f.get();
}
 
// Аналогичный код с std::execution
auto result = stdexec::schedule(sch)
    | stdexec::bulk(data.size(), [&data](size_t i) { 
        return process_item(data[i]);
    })
    | stdexec::then([](std::vector<int> results) {
        return std::accumulate(results.begin(), results.end(), 0);
    });
int sum = stdexec::sync_wait(result).value();
Тестирование на больших объёмах данных показывает, что подход с std::execution обеспечивает до 30% прирост производительности по сравнению с std::async. Основные причины:
1. Отсутствие лишних выделений памяти и синхронизации для каждой отдельной задачи.
2. Более эффективное планирование работы в пуле потоков.
3. Возможность объединять и переупорядочивать операции на этапе компиляции.

Особенно заметен прирост производительности при выполнении большого количества мелких задач. В таких сценариях накладные расходы традиционных подходов, связанные с созданием и планированием задач, часто перевешивают пользу от параллелизма.
Важную роль в оптимизации играют планировщики (schedulers). Различные типы планировщиков могут быть настроены под конкретные задачи:

C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Планировщик, оптимизированный для работы с GPU
gpu_context ctx;
auto gpu_sch = ctx.get_scheduler();
 
// Планировщик для работы с I/O
io_context io_ctx;
auto io_sch = io_ctx.get_scheduler();
 
// Комбинирование планировщиков для гибридных вычислений
auto hybrid_task = stdexec::schedule(io_sch)
    | stdexec::then([](auto data) { return preprocess(data); })
    | stdexec::then(stdexec::continues_on(gpu_sch), [](auto preprocessed) {
        return gpu_process(preprocessed);
    })
    | stdexec::then(stdexec::continues_on(sch), [](auto result) {
        return finalize(result);
    });
Такой подход позволяет эффективно распределять различные этапы обработки между специализированными исполнителями. Например, операции ввода-вывода направляются на I/O-планировщик, вычислительно сложные задачи — на GPU, а результаты обрабатываются в CPU-пуле.

Исследования, проведённые в Meta (ранее Facebook), показывают, что правильный выбор планировщика может дать до 5 раз больше производительности для некоторых типов задач. В их работе "Efficient Composition of Asynchronous Operations" (2022) описаны методики оптимизации, многие из которых нашли своё отражение в std::execution.

Для достижения максимальной производительности важно также учитывать особенности кэширования данных. В многопоточной среде неэффективная работа с кэшем может привести к значительным потерям. Используя bulk, можно организовать работу так, чтобы данные, обрабатываемые одним потоком, были локализованы в памяти:

C++
1
2
3
4
5
6
7
8
// Переопределение разбиения данных для лучшей локальности кэша
stdexec::bulk_schedule(sch, data.size(), [&](size_t i) { /* ... */ }, 
    [](size_t size) {
        size_t hardware_threads = std::thread::hardware_concurrency();
        size_t chunk_size = (size + hardware_threads - 1) / hardware_threads;
        // Настройка оптимального размера блока и стратегии разделения
        return std::make_pair(chunk_size, stdexec::strided_policy);
    });
Эти настройки позволяют адаптировать выполнение кода под конкретное оборудование и характер данных, что особенно важно для высокопроизводительных систем.
Ещё одним важным аспектом оптимизации является избежание ненужных копирований данных. std::execution предоставляет механизмы для эффективной передачи данных между этапами обработки, включая возможность использования семантики перемещения:

C++
1
2
3
4
5
6
7
8
auto move_sender = stdexec::schedule(sch)
    | stdexec::then([]() { 
        return std::make_unique<LargeObject>(); 
    })
    | stdexec::then([](std::unique_ptr<LargeObject> obj) {
        // obj перемещен сюда, без копирования
        return process(std::move(obj));
    });
Благодаря этому подходу, даже при работе с большими объемами данных или сложными объектами, накладные расходы на передачу между этапами обработки минимальны.

Ложка дёгтя



Несмотря на все преимущества std::execution, важно понимать его ограничения и потенциальные проблемы.
В первую очередь, это довольно сложная концептуальная модель, требующая времени на освоение. Разработчикам, привыкшим к более простым абстракциям вроде std::thread или std::async, может потребоваться значительное время для перехода.

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

Производительность std::execution не постоянна. Хотя в большинстве случаев она превосходит традиционные подходы, в некоторых специфических сценариях накладные расходы на абстракцию могут превышать выигрыш от параллелизма, особенно для очень маленьких задач.

Существуют и альтернативные решения, которые могут быть более подходящими в определённых контекстах:
  • Корутины C++20 предлагают более интуитивный синтаксис для последовательного асинхронного кода.
  • Библиотеки вроде HPX или TBB имеют более зрелые реализации параллельных алгоритмов.
  • Для специфических задач низкоуровневое управление потоками может дать лучший контроль.

Как проинициализировать std::stack<const int> obj ( std::stack<int>{} );
добрый день. вопрос в коде: http://rextester.com/VCVVML6656 #include &lt;iostream&gt; #include &lt;stack&gt; //-std=c++14 -fopenmp -O2 -g3...

Не освобождается память std::string после использования std::bind
Всем привет! Есть система, которая подгружает из внешних библиотек функции, упаковывает их в std::bind и заносит в std::map&lt;std::string,...

MutationObse­­­rver не перехватывае­­­т программные события
Подскажите пожалуйста, вот ставлю MutationObserver на элемент к примеру ввода. Затем просто веду курсор мышки на элемент ввода и MutationObserver -...

Не получается изменить имя родительског­­­­­о блока в цикле массива
Есть функция, которая печатает имя пользователя и его числа. При выводе результата в echo(я эти две строки пометил комментами) я создаю...

Найти подстановку, при которой заданное множ-во дизъюнктов~P(x)~Q(g(a),y)Q(x,f(x))∨R(y)P(x)∨Q(x,f(­­­x))становится невыполн
Найти подстановку, при которой заданное множество дизъюнктов ~P(x) ~Q(g(a),y) Q(x,f(x))∨R(y) P(x)∨Q(x,f(x)) становится невыполнимым. ...

Блокировка интерфейса pyside (Qt) при реализации многопоточны­­­­х приложений
Здравствуйте. Реализовал приложение для опроса (пинговки) серверов, при помощи TCP запросов. Отправка запросов и прием ответов осуществляются в...

STEAM VR , Liv, синхронизаци­­­­­­­я видео в реальности и Vr( tilt brush )
Здравствуйте, у меня задача настроить качественную запись видео художника рисующего в vr ( в программах tilt brush , adobe medium в очках oculus...

Как сделать аутентификац­ия по SMS без пароля с использовани­ем Xamarin
Здравствуйте подскажите пожалуйста, как можно сделать чтобы когда пользователь вводил номер телефона, ему отправлялось смс с кодом, который он бы...

Как сделать, чтобы в случае ошибки увеличения ЗП должна отправлять уведомление администрато­­­ру, но не пользователю­­­?
Задание: Давайте напишем функцию, которая будет увеличивать зарплату сотруднику с наименьшей зарплатой. Вам нужно Получает данные по...

Видеорегиста­­­тор NVR8016
Здравствуйте Помогите сбросить пароль на видеорегистаторе NVR8016

Неисправност­­­ь планок SDRAM?
Из того, что нашлось в закромах, получилась ретросборка на мат. плате с 370-м сокетом, докупил к ней две планки SDRAM PC-133 по 256 Мб каждая...

Использование std::execution для MinGW-8.3.0 компилятора
Приветствую, Пытаюсь скомпилировать следующий код из командной строки: main.cpp #include &lt;execution&gt; #include &lt;algorithm&gt; ...

Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 1
Комментарии
  1. Старый комментарий
    Есть ещё ложка дёгтя: в отличие от акторного подхода (например, в моём rotor'е) отправитель должен знать (так или иначе) исполнителя в момент создания задачи, т.е. знать шедулер на который отравит задачу исполняться "куда надо".
    Запись от basiliscos размещена 29.04.2025 в 12:58 basiliscos на форуме
 
Новые блоги и статьи
Жизня: рисунок укладки багажа, сделанный клодом
anaschu 21.08.2026
Сделал 15 снимков, он по снимкам сделал схему.
Был там один разговор по поводу свободы в материальном мире.
kumehtar 19.08.2026
Суть: рассматривается живое существо, оказавшееся внутри довольно странной системы (этого мира) и пытающееся обустроить в ней свой кусок пространства. Жизнь действительно предъявляет каждому. . .
Когда логика программы не спасает от человеческих ошибок
Maks 18.08.2026
В последнее время всё чаще и чаще сталкиваюсь с таким явлением, как абсолютная невнимательность (или глупость) пользователей. Проявляется это чаще всего на работе в коллективе. Допустим, человек с. . .
Лето уходит
kumehtar 17.08.2026
Мысли в слух
kumehtar 17.08.2026
Забавно, насколько сейчас стала доступна информация. Например о магии, духовном развитии, медитациях, и других подобных направлениях, ранее зачастую тайных, передаваемых от учителя к ученику. Хотя. . .
Перемещение строк из ТЧ в другой документ с учетом текущего пробега
Maks 17.08.2026
Реализация из решения ниже выполнена на примере нетипового документа "Автозапчасти", с ТЧ "Шины". За основу взят алгоритм отсюда: https:/ / www. cyberforum. ru/ blogs/ 359708/ 10838. html Задача: . . .
Саморегулирующийся социальный контракт для сервера cross-section.
Hrethgir 14.08.2026
С кодом конечно таких глубоких размышлений пока не было, впрочем я уже привык к алгоритмизации. Суть предмета записи: снова в диалоге с нейросетью (я взял пока себе ник для учётки админа - Rector). . . .
Часы электронные
Uhbif79 12.08.2026
Выкладываю программу часов. Программа позволяет: 1. Использовать системное время и дату, 2. Есть возможность вводить время и дату вручную. 3. Реализованы 2 будильника: начало и конец рабочего дня. . . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru