Доброго дня!
В очередной раз пытаюсь разобраться с короутинами из C++20. Среди того, что не понимаю, есть один момент, который можно обозначить как «идеология обращения с асинхронными сущностями». Попробую пояснить о чем речь.
Большинство примеров использования короутин демонстрируют co_await в одну операцию. Что-то вроде:
auto socket = co_await socket_factory::connect(ip);
co_await socket.send(request_packet);
auto reply = co_await socket.receive();
...
Но, насколько я смог понять, в реальности за co_await something() стоит два шага:
- Вызов something() и получение некого Task-а как результат формирования короутины something (под Task-ом понимается тип возвращаемого something() значения, это может быть и не Task, а какой-нибудь generator или еще что-то, что хранит в себе coroutine_handle из something()).
- Применение оператора co_await к возвращенному из something() Task-у. С последующей цепочкой вызовов await_ready/await_suspend/await_resume для Awaiter-а (или самого Task-а, если отдельного Awaiter-а нет).
Поэтому, при желании, программист может переписать строчку co_await socket_factory::connect(ip); в более развернутом виде:
// Получили в свое распоряжение короутину connect.
auto connect_task = socket_factory::connect(ip);
... // Тут курим бамбук.
// И только сейчас толкаем ее на выполнение.
auto socket = co_await connect_task;
Теперь к сути вопроса.
Допустим, у меня есть очередь сообщений и я хочу сделать к ней async_receive, который бы представлял из себя короутину. Т.е. что-то вроде:
async_receive_task_t async_receive(message_queue_t queue) {
... // Какие-то действия с co_yield/co_return.
}
И чтобы к этому async_receive можно было применять co_await:
auto msg = co_await async_receive(commands_queue);
Сейчас у меня есть мысль, что в async_receive сразу при входе можно делать попытку чтения из очереди. И если очередь не пуста, то немедленно доставать сообщение оттуда.
Такой «энергичный» подход вроде как хорош тем, что не нужно тратить время если очередь сообщений не пуста. Сразу же внутри async_receive получаем сообщение, наш awaiter_t об этом узнает и вернет true из await_ready. Соответственно, не придется делать попытку приостановить ту короутину, в которой обратились к async_receive.
Однако, меня смущает потенциальная возможность вот какого сценария:
auto receive_coro = async_receive(command_queue); // (1)
// Теперь у нас на руках есть короутина async_receive, но в приостановленном
// состоянии.
...
... // Еще какие-то действия.
...
if(something_went_wrong) return; // Дальше не идем.
// Только сейчас нам нужно сообщение из очереди.
use(co_await receive_coro); // (2)
При «энергичном» подходе если в command_queue было сообщение, то это сообщение будет извлечено в точке (1). Однако, если дело до точки (2) не дойдет, то сообщение будет потеряно.
Что мне кажется неправильным. Ведь co_await-а не было. А значит и явной попытки взять сообщение в обработку не было.
Возможно, правильным был бы «ленивый» подход. Сама короутина async_receive создавалась бы в приостановленном состоянии, а первая попытка чтения из очереди была бы в awaiter_t::await_suspend. И чтобы awaiter_t::await_suspend возвращал true только если очередь пуста и нужно приостанавливаться до появления в ней сообщений (соответственно, если в awaiter_t::await_suspend сообщение удалось взять, то возвращается false).
Чтобы было еще более понятно, допустим, что есть синхронный аналог, sync_receive, который всегда возвращает взятое из очереди сообщение (и блокируется если очередь пуста). В случае синхронного кода можно написать:
auto msg = sync_receive(queue);
log(debug, "msg extracted, type={}", msg->type());
...
if(something_went_wrong) return;
...
log(debug, "start processing msg");
use(msg);
log(debug, "complete processing msg");
И по логам мы точно отследим когда сообщение было взято из очереди. Даже если оно не было обработано из-за раннего return-а, то судьба сообщения становится понятна.
В случае же с async_receive написать в таком же стиле не получится. Т.е.:
auto receive_coro = async_receive(queue); // (1)
...
if(something_went_wrong) return;
...
auto msg = co_await receive_coro;
log(debug, "msg extracted, type={}", msg->type()); // (3)
log(debug, "start processing msg");
use(msg);
log(debug, "complete processing msg");
Если сообщение реально было изъято из очереди в точке (1), но до точки (3) мы не дошли, то следов сообщения у нас не будет. Получится, что иногда сообщения теряются, но непонятно где и как.
Собственно весь мой вопрос – это попытка понять, как в мире современного С++ принято относится к коду вида:
auto task = some_call(); // (1)
...
auto result = co_await task; // (2)
Я вижу два возможных и вполне себе логично обоснованных варианта:
- В точке (1) некая операция инициируется и выполняется параллельно пока основной код идет из (1) в (2). В точке (2) мы либо забираем уже готовый результат операции, начатой в (1), либо приостанавливаемся, если результата еще не было.
- В точке (1) мы только создаем ресурсы для некой операции, но сама операция еще не начата. Операция начинается в точке (2) и, если сразу ее результат получить не получится, то нас приостановят пока этот самый результат не готов.
И нужно выбрать какой из этих вариантов взять за основу для async_receive чтобы это вызывало как можно меньше «WTF?»

