I'm trying to implement some primitive called co_spawn_guard
that takes the same arguments as asio::co_spawn
runs passed coroutine and returns shared_ptr
. This shared_ptr
points to coroutine guard class and when this class is destroyed it emits cancellation signal of spawned coroutine.
All sounds pretty easy, but I get segfault and don't know why. I would be grateful for some hints.
#include <cstdint>
#include <list>
#include <vector>
#include <iostream>
#include <concepts>
#include <type_traits>
#include <memory>
#include <thread>
#include <chrono>
#define ASIO_HAS_STD_SYSTEM_ERROR
#include <boost/asio.hpp>
#include <boost/asio/experimental/channel.hpp>
#include <boost/asio/experimental/as_tuple.hpp>
using namespace boost;
class coro_guard;
template<typename Executor, typename Awaitable, typename CompletionToken>
std::shared_ptr<coro_guard> co_spawn_guard(Executor, Awaitable&&, CompletionToken&&);
class coro_guard : public std::enable_shared_from_this<coro_guard>
{
public:
template<typename Executor, typename Awaitable, typename CompletionToken>
friend std::shared_ptr<coro_guard> co_spawn_guard(Executor, Awaitable&&, CompletionToken&&);
template<typename... Args>
[[nodiscard]] static std::shared_ptr<coro_guard> create(Args&&... args)
{
return std::shared_ptr<coro_guard>(new coro_guard(std::forward<Args>(args)...));
}
coro_guard(const coro_guard&) =delete;;
coro_guard(coro_guard&&) = delete;
coro_guard& operator=(const coro_guard&) = delete;
coro_guard& operator=(coro_guard&&) = delete;
~coro_guard()
{
asio::post(ex, [cancellation_signal_ptr = cancellation_signal_ptr_]()
{
cancellation_signal_ptr->emit(asio::cancellation_type::terminal);
});
}
private:
asio::any_io_executor ex;
std::shared_ptr<asio::cancellation_signal> cancellation_signal_ptr_;
explicit coro_guard(asio::any_io_executor e) : ex{e}, cancellation_signal_ptr_{new asio::cancellation_signal{}}{}
};
template<typename Awaitable>
asio::awaitable<void> internal_coro(Awaitable&& a, std::shared_ptr<asio::cancellation_signal> ptr)
{
std::cout<<"DEBUG internal_coro enter\n";
co_await std::move(a);
std::cout<<"DEBUG internal_coro exit\n";
co_return;
}
template<typename Executor, typename Awaitable, typename CompletionToken>
std::shared_ptr<coro_guard> co_spawn_guard(Executor ex, Awaitable&& a, CompletionToken&& token)
{
std::shared_ptr<coro_guard> guard =coro_guard::create(ex);
asio::co_spawn(ex, internal_coro(std::move(a), guard->cancellation_signal_ptr_), asio::bind_cancellation_slot(guard->cancellation_signal_ptr_->slot(), std::forward<CompletionToken>(token)));
return guard;
}
asio::awaitable<void> test_coro()
{
std::cout<<"DEBUG internal_coro enter\n";
std::cout<<"DEBUG internal_coro exit\n";
co_return;
}
int main()
{
asio::io_context io_context;
auto ptr =co_spawn_guard(io_context.get_executor(), test_coro(), asio::detached);
io_context.run();
std::cout<<"dupa\n";
}
Coros cannot take their arguments by reference unless their lifetime is guaranteed. Change this:
asio::awaitable<void> internal_coro(Awaitable& a, std::shared_ptr<asio::cancellation_signal> ptr) {
To
asio::awaitable<void> internal_coro(Awaitable a, std::shared_ptr<asio::cancellation_signal> ptr) {
Also, std::move
on a "universal reference" is unsafe. Use std::forward
instead here:
asio::co_spawn(ex, internal_coro(std::move(a), guard->cancellation_signal_ptr_),
asio::bind_cancellation_slot(guard->cancellation_signal_ptr_->slot(),
std::forward<CompletionToken>(token)));
Should be
asio::co_spawn(ex, internal_coro(std::forward<Awaitable>(a), guard->cancellation_signal_ptr_),
asio::bind_cancellation_slot(guard->cancellation_signal_ptr_->slot(),
std::forward<CompletionToken>(token)));
Finally, I feel this is probably all a bit over-complicated and has some glaring design flaws. For example, you suggest you accept arbitrary completion tokens on co_spawn_guard
. However most of them will not make any sense since you neglect to use the value returned by asio::co_spawn
.