Экспериментируя с 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
и сможет немедленно запустить обработчик.
Пока всё хорошо, теперь давайте раскроем кое-какое запутанное поведение...
Оказывается, что assert
ing spawn_strand == co_await boost::asio::this_coro::executor
после строки (*)
не дает сбоя. Мы могли бы предположить, что в этот момент исполнение переключилось на switch_strand
и на самом деле просто подтвердило этот факт. Поэтому на данный момент я ожидаю, что co_await boost::asio::this_coro::executor
будет сравниваться с switch_strand
вместо spawn_strand
.
Если в строке (*)
мы заменим переданную в post
цепочку на spawn_strand
, результат изменится на:
waking up
calling handler
Я читал другие ответы (например, this или this) относительно исполнителей, предоставляемых post
и bind_executor
, и обычно они предполагают, что первый служит простым запасным вариантом на случай, если у обработчика нет связанного обработчика. Это не соответствует наблюдаемому результату.
Теперь давайте возьмем изменение предыдущего абзаца и добавим
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
. В конечном итоге кажется, что после строки (*)
сопрограмма запускается на обеих цепочках одновременно, что очень раздражает.
Пункты 2 и 3 одинаково применимы, когда вместо замены стойки, переданной на post
, мы заменяем нить, переданную на bind_handler
, на spawn_strand
.
Как можно объяснить эти странные наблюдения? Мне кажется, что в дополнение к обычному функционированию исполнителей по вызову обработчиков, boost::asio::awaitable
связаны с дополнительным исполнителем (а именно тем, который предоставляется co_spawn
и доступен через this_coro::executor
), постоянно присутствующим во время выполнения сопрограммы. Однако я нигде не нашел объяснений этого; ни документации по поддержке сопрограмм Boost.Asio C++20, ни ответов на связанные вопросы здесь. Поэтому я не верю, что на самом деле все работает именно так.
Утверждение
assert(spawn_strand == co_await asio::this_coro::executor);
Это тавтология. Вы буквально породили коро на пряди, так что это будет прядь коро. Что вы, вероятно, намеревались проверить:
assert(spawn_strand.running_in_this_thread());
Это не удастся после строки (*)
.
Поведение соответствует связанным ответам. Ключевым моментом является то, что явный аргумент исполнителя является запасным вариантом токена завершения.
Он будет использоваться, если ни один исполнитель не привязан. Когда ты публикуешь посты голым лямбды, это так.
Связанные ответы в основном касаются того факта, что, например. asio::use_awaitable
имеет исполнителя сопрограммы в качестве связанного с ним исполнителя, что означает, что явный исполнитель не будет использоваться для завершения, если он не переопределен, например, bind_executor
.
Я думаю, что предыдущий уже должен это прояснить.
В конечном итоге кажется, что после строки (*) сопрограмма запускается на обеих цепочках одновременно, что очень раздражает.
Хорошие новости: вы можете быть на многих направлениях (и не зря, например, если вам нужен синхронизированный доступ к нескольким ресурсам)¹.
Еще лучше осознавать, что любая сопрограмма по определению является отдельной логической цепочкой. Фактически, стековые сопрограммы явно оборачивают своего исполнителя в цепочку. Сопрограммам C++20, возможно, не нужно этого делать, но спецификация языка определяет возобновление таким образом, чтобы гарантировать последовательное выполнение тела сопрограммы.
(думаю, здесь нет вопросов)
В этом ответе показано, как переключить Coro на цепочку и снова отключить эксклюзивность пряди: ASIO: co_await, вызываемый для запуска на цепочке . Внизу находится ссылка на демонстрацию того, как «предположение» нескольких цепочек одновременно работает на практике: Как использовать asio::strand в библиотеке, которая предоставляет как блокирующие, так и асинхронные функции
¹ на самом деле это было мое поверхностное размышление; Если подумать немного дальше, станет ясно, что от этого нельзя зависеть с пользой, см. https://stackoverflow.com/a/78597026/85371
Пожалуйста, посмотрите Какой исполнитель производит co_await boost::asio::this_coro::executor?.
...а также Boost Asio: Как запустить один обработчик в нескольких независимых цепях?.
Спасибо, что разделили вопросы на удобные части. Это очень помогает с целенаправленными ответами. Признаюсь, что я немного поторопился с «рационализацией» многоплановых наблюдений, и подробно описываю свое улучшение понимания в этом конкретном посте.
Вместе с ответами на вопросы в моих предыдущих комментариях, здесь также дан ответ на мой вопрос. Что меня больше всего смутило, так это 1) co_await boost::asio::this_coro::executor
не возвращение Executor
сопрограммы, которая в данный момент возобновляется, и 2) running_in_this_thread()
«произвольно» возврат true
для цепочек, которые не были указаны в качестве исполнителя.
Большое спасибо за ваш ответ! Хотя, честно говоря, для меня все еще не прояснилось. Я продолжаю читать (в основном ваши) ответы здесь, в связанных постах и других, которые нахожу с помощью поиска Google снова и снова, но просто не понимаю всей картины. Я чувствую, что все еще упускаю какой-то важный ключевой момент или у меня есть серьезное заблуждение, вводящее меня в заблуждение. Я попытаюсь разбить некоторые из моих предположений на отдельные вопросы и позже вернусь сюда, чтобы попытаться понять этот ответ.