In C++, the coroutine model can be simplified to the evaluations of three methods: await_ready
, await_suspend
, and await_resume
. Instead, we only have one in Rust, the poll
. For example, we have a coroutine that is suspended(corresponding to Pending
) the first time the await expression is evaluated and continues to evaluate the following code(corresponding to Ready
) after it when the second time. In C++, the code is
#include <iostream>
#include <coroutine>
struct Task{
struct promise_type;
using handle = std::coroutine_handle<promise_type>;
struct promise_type{
auto initial_suspend(){
return std::suspend_never{};
}
auto final_suspend() noexcept{
return std::suspend_always{};
}
void unhandled_exception(){}
void return_void(){}
auto get_return_object(){
return Task{handle::from_promise(*this)};
}
};
handle coro;
};
struct SecondTimes{
bool await_ready(){
return false;
}
void await_suspend(std::coroutine_handle<>){
}
void await_resume(){}
};
Task fun(){
std::cout<<"begin\n";
co_await SecondTimes{};
std::cout<<"end\n";
}
int main() {
auto r = fun();
r.coro.resume();
}
Instead, in Rust, the code is
struct SecondTimes{
is_first:bool
}
impl Future for SecondTimes{
type Output = ();
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> std::task::Poll<Self::Output> {
if self.is_first == true{
self.get_mut().is_first = false;
std::task::Poll::Pending
}else{
std::task::Poll::Ready(())
}
}
}
async fn fun(){
println!("begin");
SecondTimes{is_first:true}.await;
println!("end");
}
For simplification, we just need to watch fun
. If we desugar them, we will get this pseudo code in C++
auto fun(short& state, SecondTimes & obj, std::coroutine_handle<> p){
switch(state){
case 0:
{
std::cout<<"begin\n";
state = 1;
}
break;
case 1:
{
obj.await_ready();
obj.await_suspend(p);
state = 2;
}
break;
case 2:
{
obj.await_ready();
std::cout<<"end\n";
state = 3;
}
break;
case 3:
{
// may destroy coroutine's inner data
}
}
}
Instead, in Rust, we may get the following (pseudo)code for fun
fn fun(state:& mut u16, obj: std::pin::Pin<&mut SecondTimes>, ctx:&mut std::task::Context<'_>)->(){
match state{
0=>{
println!("begin");
*state = 1;
}
1 =>{
match obj.poll(ctx){
Pending=>{
return;
}
Ready=>{
println!("end");
*state = 2;
}
}
}
2=>{
// may destroy coroutine's inner data
}
}
}
The difference between them is C++ maintains the inner state of SecondTimes
for us while Rust requires us manually maintain the inner state by ourselves. Specifically, C++ uses await_ready
and await_suspend
, and whenever the coroutine is resumed, such two methods will never be evaluated again, instead, only await_resume
is guaranteed to be evaluated. By contrast, in Rust, the language only evaluates poll
, which means we need the help of SecondTimes::is_fist
to record that we have evaluated SecondTimes::poll
once, which is equivalent to await_ready
and await_suspend
, and next time when the control flow again evaluates SecondTimes::poll
, we should first read the state of SecondTimes::poll
to tell us whether the evaluation of SecondTimes::poll
is the second time and then return Ready
if it is.
Fairly, I wonder which model is designed better? And why?