Reputation: 81
I have implemented my own experimental coroutine awaiter object to grasp the awaiter rationale. I call a coroutine which is foo in my example. In await_suspend method, I invoked a thread and this thread executes a job thus at the end of the job it will resume the awaiter.
However even if I control the life cycle of the tread with unique_ptr and control the joinable in the awaiter destructure, somehow handle.resume generates segfault.
Last but not least, when I use jthread instead of thread it works fine. I could not figure out what was missing here.
#include <coroutine>
#include <iostream>
#include <thread>
struct ReturnObject {
struct promise_type {
ReturnObject get_return_object() {
auto handle = std::coroutine_handle<promise_type>::from_promise(*this);
return ReturnObject{handle};
}
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() {
}
// void return_void() noexcept {
// }
void return_value(int val) noexcept {
this->value = val;
}
std::suspend_always yield_value(int val) {
this->value = val;
return std::suspend_always{};
}
double get_value() const noexcept { return value; }
void set_value(double val) noexcept { value = val; }
private:
double value{3.14};
};
std::coroutine_handle<promise_type> h_;
ReturnObject(std::coroutine_handle<promise_type> h) : h_{h} {
}
operator std::coroutine_handle<promise_type>() { return h_; }
int get_value() const {
return h_.promise().get_value();
}
~ReturnObject() {
h_.destroy();
}
};
void do_work(std::coroutine_handle<ReturnObject::promise_type>& h) {
std::cout << "do_work\n";
h.resume();
}
struct SuspendAlways {
void await_suspend(std::coroutine_handle<ReturnObject::promise_type> h) {
std::cout << "await suspend\n";
th = std::make_unique<std::thread>(do_work, std::ref(h)); //(1)
//std::jthread (&SuspendAlways::do_work, this, std::ref(h)); //(2)
}
void await_resume() {
std::cout << "await_resume\n";
}
bool await_ready() const noexcept { return false; }
~SuspendAlways() {
std::cout << "~SuspendAlways\n";
if (th->joinable()) th->join();//(1)
}
std::unique_ptr< std::thread> th;//(1)
};
ReturnObject foo() {
std::cout << "1. Hello World!\n";
co_await SuspendAlways{};
std::cout << "2. Hello World!\n";
}
int main(int argc, char **argv) {
auto ret = foo();
using namespace std::chrono_literals;
std::this_thread::sleep_for(5000ms);
std::cout << std::boolalpha << ret.h_.done() << std::endl;
}
Upvotes: 2
Views: 109
Reputation: 81
according to the feedback, I resolved the issue, thanks! The first thing I removed the std::ref to prevent the dangling handler The second improvement is I detached the thread from the awaiter object. I also learned that, If I resume the coroutine from another thread, the remaining coroutine body after co_await can be solely executed from that thread.
#include <coroutine>
#include <iostream>
#include <thread>
struct ReturnObject {
struct promise_type {
ReturnObject get_return_object() {
auto handle = std::coroutine_handle<promise_type>::from_promise(*this);
return ReturnObject{handle};
}
std::suspend_never initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() {
}
void return_void() noexcept {
}
std::suspend_always yield_value(int val) {
this->value = val;
return std::suspend_always{};
}
double get_value() const noexcept { return value; }
void set_value(double val) noexcept { value = val; }
private:
double value{3.14};
};
std::coroutine_handle<promise_type> h_;
ReturnObject(std::coroutine_handle<promise_type> h) : h_{h} {
}
operator std::coroutine_handle<promise_type>() { return h_; }
int get_value() const {
return h_.promise().get_value();
}
~ReturnObject() {
h_.destroy();
}
};
class executor {
public:
void do_work(std::coroutine_handle<ReturnObject::promise_type> h) {
std::cout << "do_work\n";
thread = std::thread([h] {
h.resume();
});
}
std::thread thread;
~executor() {
if (thread.joinable()) {
thread.join();
}
}
};
struct SuspendAlways {
SuspendAlways(executor& _executor) : executor_(_executor) {}
void await_suspend(std::coroutine_handle<ReturnObject::promise_type> h) {
std::cout << "await suspend\n";
executor_.do_work(h);
}
void await_resume() {
std::cout << "await_resume\n";
}
bool await_ready() const noexcept { return false; }
~SuspendAlways() {
std::cout << "~SuspendAlways\n";
}
executor& executor_;
};
ReturnObject foo(executor& _executor) noexcept {
std::cout << "1. Hello World!\n";
std::cout << "thread id: " << std::this_thread::get_id() << "\n";
co_await SuspendAlways{_executor};
std::cout << "2. Hello World!\n";
std::cout << "thread id: " << std::this_thread::get_id() << "\n";
}
int main(int argc, char **argv) {
executor _executor;
auto ret = foo(_executor);
using namespace std::chrono_literals;
std::this_thread::sleep_for(5000ms);
std::cout << std::boolalpha << ret.h_.done() << std::endl;
}
Upvotes: 0
Reputation: 54737
You have a number of problems in that code.
The most obvious one is that you pass the coroutine handle to the thread by reference:
th = std::make_unique<std::thread>(do_work, std::ref(h));
Note that ref(h)
is a reference to a function argument, so it becomes dangling as soon as await_suspend
returns. You should pass around the coroutine_handle
by value instead, it is very cheap to copy.
Next, you resume the coroutine on the thread, which will cause await_resume
to be called and since your awaitable is a temporary, cause the awaitable to be destroyed when the coroutine body continues executing, so ~SuspendAlways
will be called. But that awaitable is also the owner of the thread that the coroutine is now running on. So you are pulling the rug out from underneath your own feet. Your attempt to join the thread on itself is undefined behavior.
If you resolved that issue, you would drop off the end of the coroutine body later, but your promise_type
does not define a return_void
function. That is also undefined behavior.
I'd suggest you restructure your code to clearly separate the parts that manage the executing thread from the work that is executing on those threads. In particular, try to avoid having the coroutine manage its own executing thread, as that is very difficult to get right and not a very robust design to work with.
Upvotes: 1
Reputation: 2057
std::thread
's destructor will terminate the program if you don't join or detach it before it is destroyed. std::jthread
joins on destruction instead of terminating the program. Perhaps your std::thread object is being destroyed before it is detached or joined?
Upvotes: 1