Boost Asio: исполнители в сопрограммах C++20

Экспериментируя с boost::asio::awaitable и Executor, я продолжаю наблюдать довольно запутанное поведение, которое мне хотелось бы лучше понять.

Подготовка

Пожалуйста, обратите внимание на следующую программу:

int main(int argc, char const* args[])
{
    boost::asio::io_context ioc;
    auto spawn_strand = boost::asio::make_strand(ioc);

    boost::asio::co_spawn(spawn_strand,
        [&]() -> boost::asio::awaitable<void>
        {
            auto switch_strand = boost::asio::make_strand(ioc);
            co_await boost::asio::post(switch_strand, // (*)
                boost::asio::bind_executor(switch_strand, boost::asio::use_awaitable));

            boost::asio::post(spawn_strand, [](){
                std::cout << "calling handler\n";
            });

            std::this_thread::sleep_for(std::chrono::seconds(3));
            std::cout << "waking up\n";
        },
        boost::asio::detached);

    std::jthread threads[3]; // provide enough threads to serve strands in parallel
    for (auto& thread : threads)
        thread = std::jthread{ [&]() { ioc.run(); }};
}

Как и ожидалось, эта программа выводит:

calling handler
waking up

В частности, "calling handler" печатается перед "waking up", поскольку после строки (*) сопрограмма больше не запускается spawn_strand. Таким образом, последний не будет заблокирован sleep_for и сможет немедленно запустить обработчик.

Пока всё хорошо, теперь давайте раскроем кое-какое запутанное поведение...

Наблюдения

  1. Оказывается, что asserting spawn_strand == co_await boost::asio::this_coro::executor после строки (*) не дает сбоя. Мы могли бы предположить, что в этот момент исполнение переключилось на switch_strand и на самом деле просто подтвердило этот факт. Поэтому на данный момент я ожидаю, что co_await boost::asio::this_coro::executor будет сравниваться с switch_strand вместо spawn_strand.

  2. Если в строке (*) мы заменим переданную в post цепочку на spawn_strand, результат изменится на:

    waking up
    calling handler
    

    Я читал другие ответы (например, this или this) относительно исполнителей, предоставляемых post и bind_executor, и обычно они предполагают, что первый служит простым запасным вариантом на случай, если у обработчика нет связанного обработчика. Это не соответствует наблюдаемому результату.

  3. Теперь давайте возьмем изменение предыдущего абзаца и добавим

    boost::asio::post(switch_strand, [](){
        std::cout << "calling handler\n";
    });
    

    после строки (*). Обратите внимание, что на этот раз мы используем switch_strand вместо spawn_strand. Мы получим следующий результат

    waking up
    calling handler
    calling handler
    

    Это предполагает, что оба обработчика (как на switch_strand, так и на spawn_strand) заблокированы одним sleep_for. В конечном итоге кажется, что после строки (*) сопрограмма запускается на обеих цепочках одновременно, что очень раздражает.

  4. Пункты 2 и 3 одинаково применимы, когда вместо замены стойки, переданной на post, мы заменяем нить, переданную на bind_handler, на spawn_strand.

Вопрос

Как можно объяснить эти странные наблюдения? Мне кажется, что в дополнение к обычному функционированию исполнителей по вызову обработчиков, boost::asio::awaitable связаны с дополнительным исполнителем (а именно тем, который предоставляется co_spawn и доступен через this_coro::executor), постоянно присутствующим во время выполнения сопрограммы. Однако я нигде не нашел объяснений этого; ни документации по поддержке сопрограмм Boost.Asio C++20, ни ответов на связанные вопросы здесь. Поэтому я не верю, что на самом деле все работает именно так.

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
0
120
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий
  1. Утверждение

    assert(spawn_strand == co_await asio::this_coro::executor);
    

    Это тавтология. Вы буквально породили коро на пряди, так что это будет прядь коро. Что вы, вероятно, намеревались проверить:

    assert(spawn_strand.running_in_this_thread());
    

    Это не удастся после строки (*).

  2. Поведение соответствует связанным ответам. Ключевым моментом является то, что явный аргумент исполнителя является запасным вариантом токена завершения.

    Он будет использоваться, если ни один исполнитель не привязан. Когда ты публикуешь посты голым лямбды, это так.

    Связанные ответы в основном касаются того факта, что, например. asio::use_awaitable имеет исполнителя сопрограммы в качестве связанного с ним исполнителя, что означает, что явный исполнитель не будет использоваться для завершения, если он не переопределен, например, bind_executor.

  3. Я думаю, что предыдущий уже должен это прояснить.

    В конечном итоге кажется, что после строки (*) сопрограмма запускается на обеих цепочках одновременно, что очень раздражает.

    Хорошие новости: вы можете быть на многих направлениях (и не зря, например, если вам нужен синхронизированный доступ к нескольким ресурсам)¹.

    Еще лучше осознавать, что любая сопрограмма по определению является отдельной логической цепочкой. Фактически, стековые сопрограммы явно оборачивают своего исполнителя в цепочку. Сопрограммам C++20, возможно, не нужно этого делать, но спецификация языка определяет возобновление таким образом, чтобы гарантировать последовательное выполнение тела сопрограммы.

  4. (думаю, здесь нет вопросов)

В этом ответе показано, как переключить Coro на цепочку и снова отключить эксклюзивность пряди: ASIO: co_await, вызываемый для запуска на цепочке . Внизу находится ссылка на демонстрацию того, как «предположение» нескольких цепочек одновременно работает на практике: Как использовать asio::strand в библиотеке, которая предоставляет как блокирующие, так и асинхронные функции


¹ на самом деле это было мое поверхностное размышление; Если подумать немного дальше, станет ясно, что от этого нельзя зависеть с пользой, см. https://stackoverflow.com/a/78597026/85371

Большое спасибо за ваш ответ! Хотя, честно говоря, для меня все еще не прояснилось. Я продолжаю читать (в основном ваши) ответы здесь, в связанных постах и ​​других, которые нахожу с помощью поиска Google снова и снова, но просто не понимаю всей картины. Я чувствую, что все еще упускаю какой-то важный ключевой момент или у меня есть серьезное заблуждение, вводящее меня в заблуждение. Я попытаюсь разбить некоторые из моих предположений на отдельные вопросы и позже вернусь сюда, чтобы попытаться понять этот ответ.

Reizo 08.06.2024 19:14

Пожалуйста, посмотрите Какой исполнитель производит co_await boost::asio::this_coro::executor?.

Reizo 08.06.2024 19:16

Спасибо, что разделили вопросы на удобные части. Это очень помогает с целенаправленными ответами. Признаюсь, что я немного поторопился с «рационализацией» многоплановых наблюдений, и подробно описываю свое улучшение понимания в этом конкретном посте.

sehe 09.06.2024 00:27

Вместе с ответами на вопросы в моих предыдущих комментариях, здесь также дан ответ на мой вопрос. Что меня больше всего смутило, так это 1) co_await boost::asio::this_coro::executor не возвращение Executor сопрограммы, которая в данный момент возобновляется, и 2) running_in_this_thread() «произвольно» возврат true для цепочек, которые не были указаны в качестве исполнителя.

Reizo 15.06.2024 15:13

Другие вопросы по теме