c++ 20 引入了协程的特性,很多其他语言很早就已经有了,本文旨在了解协程的概念以及简单明白底层实现的原理,写成以笔记的形式,对详细内容感兴趣的可以看文末链接。

Three new language keywords: co_await, co_yield and co_return

1、definition

A coroutine is a generalisation of a function that allows the function to be suspended and then later resumed.

Coroutines are stackless: they suspend execution by returning to the caller and the data that is required to resume execution is stored separately from the stack.

Coroutine 协程

1.1、normal call

A normal function can be thought of as having two operations: Call and Return (Note that I’m lumping “throwing an exception” here broadly under the Return operation).

The Call operation creates an activation frame, suspends execution of the calling function and transfers execution to the start of the function being called.

The Return operation passes the return-value to the caller, destroys the activation frame and then resumes execution of the caller just after the point at which it called the function.

1.2、coroutine

Coroutines generalise the operations of a function by separating out some of the steps performed in the Call and Return operations into three extra operations: Suspend, Resume and Destroy.

The Suspend operation suspends execution of the coroutine at the current point within the function and transfers execution back to the caller or resumer without destroying the activation frame. Any objects in-scope at the point of suspension remain alive after the coroutine execution is suspended.

Note that, like the Return operation of a function, a coroutine can only be suspended from within the coroutine itself at well-defined suspend-points.

The Resume operation resumes execution of a suspended coroutine at the point at which it was suspended. This reactivates the coroutine’s activation frame.

The Destroy operation destroys the activation frame without resuming execution of the coroutine. Any objects that were in-scope at the suspend point will be destroyed. Memory used to store the activation frame is freed.

1.2.1、Coroutine activation frames

Since coroutines can be suspended without destroying the activation frame, we can no longer guarantee that activation frame lifetimes will be strictly nested. This means that activation frames cannot in general be allocated using a stack data-structure and so may need to be stored on the heap instead.

You can logically think of the activation frame of a coroutine as being comprised of two parts: the ‘coroutine frame’ and the ‘stack frame’.

The ‘coroutine frame’ holds part of the coroutine’s activation frame that persists while the coroutine is suspended and the ‘stack frame’ part only exists while the coroutine is executing and is freed when the coroutine suspends and transfers execution back to the caller/resumer.

1.2.2、some works

The first thing the coroutine does is then allocate a coroutine-frame on the heap and copy/move the parameters from the stack-frame into the coroutine-frame so that the lifetime of the parameters extends beyond the first suspend-point.

When a coroutine executes a return-statement (co_return according to the TS) operation it stores the return-value somewhere (exactly where this is stored can be customised by the coroutine) and then destructs any in-scope local variables (but not parameters).

The coroutine then performs either a Suspend operation (keeping the coroutine-frame alive) or a Destroy operation (destroying the coroutine-frame).

Execution is then transferred back to the caller/resumer as per the Suspend/Destroy operation semantics, popping the stack-frame component of the activation-frame off the stack.

2、An illustration

let’s say we have a function (or coroutine), f() that calls a coroutine, x(int a).

Before the call we have a situation that looks a bit like this:

Coroutine 协程

Then when x(42) is called, it first creates a stack frame for x(), as with normal functions.

Coroutine 协程

Then, once the coroutine x() has allocated memory for the coroutine frame on the heap and copied/moved parameter values into the coroutine frame we’ll end up with something that looks like the next diagram. Note that the compiler will typically hold the address of the coroutine frame in a separate register to the stack pointer (eg. MSVC stores this in the rbp register).

Coroutine 协程

If the coroutine x() then calls another normal function g() it will look something like this.

Coroutine 协程

When g() returns it will destroy its activation frame and restore x()’s activation frame. Let’s say we save g()’s return value in a local variable b which is stored in the coroutine frame.

Coroutine 协程

If x() now hits a suspend-point and suspends execution without destroying its activation frame then execution returns to f().

This results in the stack-frame part of x() being popped off the stack while leaving the coroutine-frame on the heap. When the coroutine suspends for the first time, a return-value is returned to the caller. This return value often holds a handle to the coroutine-frame that suspended that can be used to later resume it. When x() suspends it also stores the address of the resumption-point of x() in the coroutine frame (call it RP for resume-point).

Coroutine 协程

This handle may now be passed around as a normal value between functions. At some point later, potentially from a different call-stack or even on a different thread, something (say, h()) will decide to resume execution of that coroutine. For example, when an async I/O operation completes.

The function that resumes the coroutine calls a void resume(handle) function to resume execution of the coroutine. To the caller, this looks just like any other normal call to a void-returning function with a single argument.

This creates a new stack-frame that records the return-address of the caller to resume(), activates the coroutine-frame by loading its address into a register and resumes execution of x() at the resume-point stored in the coroutine-frame.

Coroutine 协程

参考&推荐阅读

Coroutine Theory:https://lewissbaker.github.io/2017/09/25/coroutine-theory

C++ Coroutines: Understanding operator co_await:https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await

C++ Coroutines: Understanding the promise type:https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type

C++ Coroutines: Understanding Symmetric Transfer:https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer

Coroutines (C++20):https://en.cppreference.com/w/cpp/language/coroutines

神秘使者到Java帝国传道协程,竟被轰了出去:https://mp.weixin.qq.com/s/cN9dC_crrEU579-AMrd5PQ

相关文章:

  • 2021-07-15
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
  • 2021-09-23
  • 2022-03-05
  • 2022-12-23
猜你喜欢
  • 2021-06-01
  • 2021-07-06
  • 2021-08-21
  • 2022-12-23
  • 2022-12-23
  • 2022-12-23
相关资源
相似解决方案