2

C++ 20 Coroutine API Memo & Exploration

 1 year ago
source link: https://keys961.github.io/2022/11/14/CppCoroutine/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C++ 20 Coroutine API Memo & Exploration

Posted by keys961 on November 14, 2022

C++ 20的Coroutine是一个函数,它支持用户态的挂起恢复(并调度到某个线程上)。

它可以长成这样:

Task<int> func_coroutine(void *data) {
  // ...
  co_await func_another_coroutine(); // suspend current coroutine
  // ...
  co_return 2; // return the value
}
Task<int> func_coroutine(void *data) {
  // ...
  co_await func_another_coroutine(); // suspend current coroutine
  // ...
  co_return 2; // return the value
}

类似于线程在CPU核心的调度,这里对应的是coroutine在线程上的调度:

  • 挂起:coroutine在某个线程中被切出

  • 恢复:coroutine被切回某个线程,该线程调用了handle.resume()

2. Coroutine核心:Promise & Awaiter

假如我们随便编写一个Task类,那么上述的代码不能通过编译,会警告我们找不到Task::promise_type。这就牵扯到coroutine的第一个核心:Promise

2.1. Promise

正如上面所述,返回值Task需要一个promise_type。这个promise_type需要实现下面的几个函数:

  • Awaiter initial_suspend():在当前coroutine开始执行后,调用它,返回的Awaiter决定本coroutine是否被挂起

  • Awaiter final_suspend():当前coroutine执行完成后(比如co_return时),调用它,返回的Awaiter决定本coroutine是否被挂起

    • 若决定挂起,需要之后在外部调用handle.destroy()销毁coroutine,并手动清理资源
  • Task get_return_object():返回这个Task对象,在最开始的时候被调用

    • Task还需要持有std::coroutine_handle<promise_type>类型的handle字段,代表当前的coroutine

因此,一个coroutine的具体执行步骤是:

  1. 调用get_return_object(),返回Task对象,里面记录了coroutine的handle

  2. 调用initial_suspend(),返回一个Awaiter,由它来决定coroutine执行前是否挂起

  3. 执行coroutine的逻辑

  4. 最后,调用final_suspend(),返回一个Awaiter,由它来决定coroutine执行后是否挂起

template<typename T, typename Exec>
struct TaskPromise {
  // 由DispatchAwaiter决定,本coroutine执行前是否被挂起
  DispatchAwaiter initial_suspend() { return DispatchAwaiter{&exec}; }
  // 本coroutine执行后永远被挂起,等待销毁
  std::suspend_always final_suspend() noexcept { return {}; }
  // 创建Task对象,记录了coroutine的handle
  Task<T, Exec> get_return_object() {
    return Task{std::coroutine_handle<TaskPromise>::from_promise(*this)};
  }
}

template<typename T, typename Exec>
struct Task {
  using promise_type = TaskPromise;
  std::coroutine_handle<> handle;
  // ... 
}
template<typename T, typename Exec>
struct TaskPromise {
  // 由DispatchAwaiter决定,本coroutine执行前是否被挂起
  DispatchAwaiter initial_suspend() { return DispatchAwaiter{&exec}; }
  // 本coroutine执行后永远被挂起,等待销毁
  std::suspend_always final_suspend() noexcept { return {}; }
  // 创建Task对象,记录了coroutine的handle
  Task<T, Exec> get_return_object() {
    return Task{std::coroutine_handle<TaskPromise>::from_promise(*this)};
  }
}

template<typename T, typename Exec>
struct Task {
  using promise_type = TaskPromise;
  std::coroutine_handle<> handle;
  // ... 
}

除此之外,还有几个比较重要的函数:

  • void unhandled_exception():若本coroutine执行出现没有捕捉的异常,该函数就会被调用

  • void return_value(T value)/void return_void():本coroutine执行co_return时,会调用该函数,将值传进去,Promise需要保存该返回值

  • Awaiter await_transform(XXX):若本coroutine中调用了co_await XXX,若XXX不是Awaiter,那么会调用该函数,将XXX转换成Awaiter。此时,本coroutine的挂起和恢复受该Awaiter控制

  • YYY yield_value(XXX):若本coroutine中调用了co_yield XXX,那么它等效于co_await prommise.yield_value(XXX)

如上文所述,Awaiter控制了coroutine的挂起和恢复,下面就说明coroutine的第二个核心:Awaiter

2.2. Awaiter

Awaiter控制了当前coroutine的挂起和恢复。它需要实现下面几个函数:

  • bool await_ready():若返回false,则会挂起本coroutine;否则不会挂起。

  • ? await_suspend(std::coroutine_handle<>):若await_ready()返回false,则会调用该函数。它可以决定参数中的coroutine(即本coroutine)什么时候唤醒,可以在函数内选择调用handle.resume()唤醒。

    • ?void,或返回true:挂起本coroutine

    • 若返回false:唤醒本coroutine

    • 若返回其它的std::coroutine_handle<>:返回的coroutine被唤醒

    • 若抛出异常:唤醒本coroutine,并抛出异常

    例如,我们希望让本coroutine在一个线程池中唤醒,可以这样:

    void await_suspend(std::coroutine_handle<> h) {
      handle = h;
      exec.execute([this]() {
        handle.resume(); // 在线程池中resume
      });
      // 但先suspend本coroutine
    }
    
    void await_suspend(std::coroutine_handle<> h) {
      handle = h;
      exec.execute([this]() {
        handle.resume(); // 在线程池中resume
      });
      // 但先suspend本coroutine
    }
    
  • T await_resume():若本coroutine被唤醒,那么它的返回值会作为co_await表达式的返回值。

    例如下面的例子,一个整数a作为co_await表达式的返回值,它就由await_resume()返回的:

    Task<void> f() {
      // ...
      int a = co_await f2(); // return an integer
      // ...
    }
    
    Task<void> f() {
      // ...
      int a = co_await f2(); // return an integer
      // ...
    }
    

3. 3个运算符:co_await, co_yield, co_return

假设我们有这样的Coroutine:

Task<int> f() {
  // ...
  co_await XXX; // For 3.1
  // ...
  co_yield YYY; // For 3.2.
  // ...
  co_return 0; // For 3.3.
}
Task<int> f() {
  // ...
  co_await XXX; // For 3.1
  // ...
  co_yield YYY; // For 3.2.
  // ...
  co_return 0; // For 3.3.
}

3.0. f()的执行

首先回顾2.1.节的内容。

  1. 调用f()前,首先调用Task::promise_type::get_return_object()创建Task对象,保存std::coroutine_handle<>实例

  2. 调用Task::promise_type::initial_suspend(),返回一个Awaiter,它会挂起并唤醒该coroutine

  3. 执行上面的3个步骤

  4. 最后调用Task::promise_type::final_suspend(),返回一个Awaiter,挂起或唤醒该coroutine

若无特别说明,Task::xxxTask指的是返回值Task<int>实例。

3.1. co_await

co_await expr:这里expr需要返回一个Awaiter

若不是Awaiter,则调用Task::promise_type::await_transform(expr)转成一个Awaiter

此时,当前coroutine(调用co_await表达式的coroutine)的挂起和唤醒就由该Awaiter决定。

  • co_await 2s:返回一个SleepAwaiter(通过2s转化而来的),它会先挂起本coroutine,然后过2秒后唤醒

  • co_await f():返回一个由Task<int>转换而成的Awaiter,该Awaiter决定是否挂起本coroutine,遵循2.2.节的规则

    • 但此时创建了一个新coroutine,它的执行遵循3.0.节的步骤

3.2. co_yield

它就是co_await的马甲,就是co_await Task::promise_type::yield_value(expr)。这里不再详述。

3.3. co_return

co_return expr:该语句作为coroutine的返回值

  • 若返回的是void:调用Task::promise_type::return_void()

  • 若返回是具体值:调用Task::promise_type::return_value(expr)

之后,coroutine将会返回,最后调用Task::promise_type::final_suspend(),其返回的Awaiter决定最后是否需要挂起这个coroutine。

4. 其它细节

4.1. 关于coroutine的恢复

Coroutine会在调用handle.resume()的线程上执行,直到它再被挂起,该线程才会执行handle.resume()下面一行代码

如下代码所示:

void resume() {
  // called by T1
  // ...
  std::cout << "Resuming..." << std::endl;
  handle.resume();
  std::cout << "Resumed" << std::endl;
  handle.destroy();
}

// 假设h()的coroutine handle就是上面的
Task<void> h() {
  std::cout << "h()" << std::endl;
}

std::suspend_always Task<void>::promise_type::initial_suspend() {
  return {};
}
void resume() {
  // called by T1
  // ...
  std::cout << "Resuming..." << std::endl;
  handle.resume();
  std::cout << "Resumed" << std::endl;
  handle.destroy();
}

// 假设h()的coroutine handle就是上面的
Task<void> h() {
  std::cout << "h()" << std::endl;
}

std::suspend_always Task<void>::promise_type::initial_suspend() {
  return {};
}

这里的h()会先被挂起(见initial_suspend()),然后线程T1调用resume()来恢复该coroutine,此时:

  • h()会在T1线程执行

  • 打印结果是:

    Resuming...
    h()
    Resumed
    
    Resuming...
    h()
    Resumed
    

4.2. 典型的Awaiter

a. std::suspend_always

永远挂起当前coroutine。

struct suspend_always {
  // 永远需要挂起
  constexpr bool await_ready() const noexcept { return false; }
  // 返回void,直接挂起coroutine
  constexpr void await_suspend(coroutine_handle<>) const noexcept {}
  constexpr void await_resume() const noexcept {}
};
struct suspend_always {
  // 永远需要挂起
  constexpr bool await_ready() const noexcept { return false; }
  // 返回void,直接挂起coroutine
  constexpr void await_suspend(coroutine_handle<>) const noexcept {}
  constexpr void await_resume() const noexcept {}
};

b. std::suspend_never

永远不挂起当前coroutine。

struct suspend_never {
  // 永远不挂起
  constexpr bool await_ready() const noexcept { return true; }
  // 该函数不会被调用
  constexpr void await_suspend(coroutine_handle<>) const noexcept {}
  constexpr void await_resume() const noexcept {}
};
struct suspend_never {
  // 永远不挂起
  constexpr bool await_ready() const noexcept { return true; }
  // 该函数不会被调用
  constexpr void await_suspend(coroutine_handle<>) const noexcept {}
  constexpr void await_resume() const noexcept {}
};

5. 相关源码和参考

这里参考了:渡劫 C++ 协程(1):C++ 协程概览 Benny Huo的文章,解释的很清楚。

然后代码也在:TempRepo/coroutine at master · keys961/TempRepo · GitHub,有注释。

此外,cppreference也有很好的解释:Coroutines (C++20) - cppreference.com

扩展性还是很好的,但需要记住非常多的API,并理清各个函数的意义,上手还是非常难的。

期待后续的更新,使得它更加易用。


0 comments

Be the first person to leave a comment!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK