c++ 协程

协程的原理和应用

 

在一些高并发应用场景中如果使用多线程(每个请求一个线程),每个请求的大部分时间可能都是阻塞在IO上,会带来很多线程切换的开销(C/C++ 线程的切换是由内核控制的);多线程以外,一般有2种其它方案,一个是用IO多路复用(如 Nginx、Redis都用 epoll 实现非阻塞网络IO),另一个就是协程,就我了解目前各种语言里面对协程的原生支持最好的就是 golang,在runtime 实现了一个协程调度器,可以在一个物理线程里面并发执行上百个协程;

 

 

协程的定义

协程的本质是一个支持挂起(suspend)和恢复(resume)的函数,也就是说我们可以暂停执行这个函数(程序执行流会回到 Caller),去做其他事情,然后在恰当的时候再恢复到离开的位置继续执行。也就是说,普通的函数只能返回一次,而协程可以返回多次。

 

例如一个普通的函数:

void Fun() {
  std::cout << 1 << std::endl;
  std::cout << 2 << std::endl;
  std::cout << 3 << std::endl;
  std::cout << 4 << std::endl;
}

Fun 是一个非常普通的函数,有4行代码,会依次执行;这个函数一旦开始,就无法暂停;

 

下面是一段 C++ 协程的不完整的例子

Result Coroutine() {
  std::cout << 1 << std::endl;
  co_await std::suspend_always{};
  std::cout << 2 << std::endl;
  std::cout << 3 << std::endl;
  co_await std::suspend_always{};
  std::cout << 4 << std::endl;
};

Result 的定义后面再谈论,现在只需要知道 Result 是按照协程的规则定义的类型,这个函数体当中的 co_await std::suspend_always{};,其中 co_await 是个关键字,它的出现,通常来说就会使得当前函数(协程)的执行被挂起,程序执行流会回到调用者。

 

C++ 的协程语义

C++ 20 开始支持协程,各个编译器的支持情况可以参考 https://en.cppreference.com/w/cpp/compiler_support

个人感觉 C++ 提供的协程语义比较原始,用的时候还需要做很多封装才行;

 

 

Awaiter

协程的函数体中的三个关键字:

  1. co_await,挂起协程,这个关键字是一个运算符,表达式操作的对象称为 可等待体(awaiter),awaiter 要实现3个函数;
    • // 返回true 表示已就绪,无需挂起,否则表示需要挂起;
      bool await_ready();
      
      // await_ready() 返回 false 时被调用,参数 coroutine_handle 表示当前协程
      // 返回值类型有多个可能,
      ? await_suspend(std::coroutine_handle<> coroutine_handle);
      
      // 协程恢复时被调用,其返回值就作为 co_await 表达式的返回值
      T await_resume();
    • 标准库当中提供了两个非常简单直接的等待体,struct suspend_always 表示总是挂起,struct suspend_never 表示总是不挂起;
  2. co_yield,可用于传值给协程的调用者;
  3. co_return,返回一个值或者从协程体返回;

所以,如果看见一个函数里面有 上面这样的关键字,那它就是一个协程;

编译器遇到 co_await 表达式,会将其翻译成下面的样子:

{
  auto&& value = <expr>;
  auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
  auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
  if (!awaiter.await_ready()) {
    using handle_t = std::experimental::coroutine_handle<P>;

    using await_suspend_result_t =
      decltype(awaiter.await_suspend(handle_t::from_promise(p)));

    <suspend-coroutine>

    if constexpr (std::is_void_v<await_suspend_result_t>) {
      awaiter.await_suspend(handle_t::from_promise(p));
      <return-to-caller-or-resumer>
    } else {
      static_assert(
         std::is_same_v<await_suspend_result_t, bool>,
         "await_suspend() must return 'void' or 'bool'.");

      if (awaiter.await_suspend(handle_t::from_promise(p))) {
        <return-to-caller-or-resumer>
      }
    }

    <resume-point>
  }

  return awaiter.await_resume();
}

可以看出来 await_suspend 的返回值:

  • 如果是 void 或者 true,当前协程挂起之后将执行权还给当初调用者;

  • 返回 false,则恢复执行当前协程;

 

标准库中提供了2个默认的 Awaiter 实现

namespace std {
  struct suspend_never {
      constexpr bool await_ready() const noexcept { return true; }        // continue current 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; }  // go back caller
      constexpr void await_suspend(coroutine_handle<>) const noexcept {}
      constexpr void await_resume() const noexcept {}
  };
}

 

 

Promise

协程的状态(返回值)可以用一个 Promise 对象来描述

struct Result {
  struct promise_type {
    ...
  };
};

promise_type 是一个接口,定义了协程执行节点中的一些回调;promise 对象会在 协程栈(coroutine frame)里面构建。

 

如果一个协程函数长这样

Result coroutine() {
  <body-statements>
}

它实际执行的时候会被翻译成:

{
  co_await promise.initial_suspend();
  try {
    <body-statements>
  } catch (...) {
    promise.unhandled_exception();
  }
FinalSuspend:
  co_await promise.final_suspend();
}

 

一个 Result 的简单实现:

struct Result {
  struct promise_type {

    // 构建返回值对象
    Result get_return_object() {
        return coroutine_handle<promise_type>::from_promise(*this);
    }    
    suspend_never initial_suspend() noexcept { return {}; }
    suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
  };  

  Result(coroutine_handle<promise_type> h) : handle(h) {}
  coroutine_handle<promise_type> handle;
};

 

这个例子里面 get_return_object 返回的是 coroutine_handle,它指向了协程栈的地址,可以用来恢复协程的执行,或者销毁协程栈

namespace std::experimental
{
  template<typename Promise>
  struct coroutine_handle;

  template<>
  struct coroutine_handle<void>
  {
    bool done() const;

    void resume();
    void destroy();

    void* address() const;
    static coroutine_handle from_address(void* address);
  };

  template<typename Promise>
  struct coroutine_handle : coroutine_handle<void>
  {
    Promise& promise() const;
    static coroutine_handle from_promise(Promise& promise);

    static coroutine_handle from_address(void* address);
  };
}

 

应用

跨线程执行

如下图,左边灰色的coroutine 函数,当它被调用的时候,被切分成了两个部分,线程1在执行完成第一部分后,线程1就继续做其他事情了。

第二部分被suspend了。当时机出现的时候,线程2(可以是线程1)就开始执行第二部分。

 

#include <iostream>
#include <experimental/coroutine>
#include <string>
#include <chrono>
#include <thread>
#include <future>
#include <time.h>

// g++ -std=c++2a -stdlib=libc++ -fcoroutines-ts test1.cc 

using namespace std;
using std::chrono::duration_cast;
using std::chrono::nanoseconds;
using std::experimental::coroutine_handle;
using std::experimental::noop_coroutine;
using std::experimental::suspend_always;
using std::experimental::suspend_never;

#define LOG(MSG) printf("[%d][%ld] %s\n", std::this_thread::get_id(), time(nullptr), MSG);

struct Awaiter {
  int value;
  std::future<void> future;

  bool await_ready() {
    // 协程挂起
    LOG("await_ready")
    return false;
  }

  void await_suspend(coroutine_handle<> handle) {
    LOG("await_suspend begin")
    // 切换线程
    future = std::async(std::launch::async, [=](){
      using namespace std::chrono_literals;
      std::this_thread::sleep_for(3s);
      value = 100;
      // 恢复协程
      auto* p_handle = const_cast<coroutine_handle<>*>(&handle);
      p_handle->resume();
    });
    LOG("await_suspend done")
  }

  int await_resume() {
    LOG("await_resume")
    // value 将作为 co_await 表达式的值
    return value;
  }
};

struct Result {
  struct promise_type {

    // 构建返回值对象
    Result get_return_object() {
        return coroutine_handle<promise_type>::from_promise(*this);
    }

    // co_return 传入参数
    void return_value(int value) {
        cout << "return_value = " << value << endl;
    }
    suspend_never initial_suspend() noexcept { return {}; }
    suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() {}

  };

  Result(coroutine_handle<promise_type> h) : handle(h) {}
  coroutine_handle<promise_type> handle;
};

Result coroutine() {
    int local = 777;
    cout << co_await Awaiter{.value = 0} << endl;
    LOG("after_suspend")
    co_return local;
}

int main() {
    Result co = coroutine();

    LOG("destroy")
    co.handle.destroy();
    cout << "main done" << endl;
}

 

执行结果:

[1304852224][1692150804] await_ready
[1304852224][1692150804] await_suspend begin
[1304852224][1692150804] await_suspend done
[1304852224][1692150804] destroy
[193277952][1692150807] await_resume
100
[193277952][1692150807] after_suspend
return_value = 777
main done

可以看到,主线程挂起后立即返回了,子线程在 resume 后会自动执行协程的后半部分代码,不需要主线程去阻塞等待子线程。

 

 

生成器

#include <iostream>
#include <experimental/coroutine>
#include <string>

// g++ -std=c++2a -stdlib=libc++ -fcoroutines-ts main.cc 

using namespace std;
using std::chrono::duration_cast;
using std::chrono::nanoseconds;
using std::experimental::coroutine_handle;
using std::experimental::noop_coroutine;
using std::experimental::suspend_always;
using std::experimental::suspend_never;


struct generator {

    typedef struct promise_type {
        generator get_return_object() noexcept {
            return coroutine_handle<promise_type>::from_promise(*this);
        }
        auto initial_suspend() noexcept { return suspend_never{}; }

        // 在 final_suspend() 挂起了协程,所以要手动 destroy
        auto final_suspend() noexcept { return suspend_always{}; }

        suspend_always yield_value(int value) {
            value_ = value;
            cout << value_ << endl;
            return {};
        }
        
        void unhandled_exception() {}
        int value_;
    };
    
    generator(coroutine_handle<promise_type> h) : handle(h) {}

    coroutine_handle<promise_type> handle;
};

generator makeGenerator() {
    cout << "coroutine start " << endl;
    //co_await suspend_always{};
    for (int i = 0; i < 10; i++) {
        co_yield i;     // go back to caller
    }
    cout << "coroutine done" << endl;
}

int main() {
    generator coro = makeGenerator();
    
    for (int i = 0; i < 10; i++) {
        coro.handle.resume();   // continue execute coroutine
    }
    
    cout << "destroy" << endl;
    coro.handle.destroy();

}

co_yield 表达式等价于 co_await promise.yield_value(<expr>)

执行结果:

coroutine start 
0
1
2
3
4
5
6
7
8
9
coroutine done
destroy

 

协程函数的调用栈

普通函数的调用栈都是在 stack 上嵌套,而协程的调用栈除了使用 stack,还会用到 heap 堆内心。

 

example:

void f() {                                // 普通函数
    Result co = x(42);
  h(co);
}

Result x(int a) {                    // 协程
    g();
  co_await ...
  ...
}

int g() {                                // 普通函数
    ...
}

void h(Result co) {
  co.handle.resume();
}

 

 

普通函数 f() 调用了 协程 x(int a),调用之前的栈

 

调用 x() 之后,跟普通函数调用一样,将函数返回值地址、函数入参、函数指针压入栈

 

协程执行的时候会在协程帧(coroutine frame)上构建 promise 对象,这里是在堆上分配内存,此时编译器也会在寄存器(rbp)里面存协程帧的指针

 

协程 x 里面又调用了一个普通函数 g(),x()的栈里面多了一个 coroframe 指针

 

 

当 g() 返回后,x() 的帧会被还原,同时 g() 的返回值 b 会被存入协程栈里面

 

接下来,协程x() 遇到 co_wait 表达式,假设被挂起后返回调用者 f(),协程返回时,一般返回给主调函数一个handle(执行协程帧),同时协程帧也会存一个RP地址(resumption-point),用于resume后找到协程挂起时的执行点。

 

返回到主调 f() 函数后,又继续执行 h(),并且在 h() 里面执行了 协程的 resume

 

参考资料:

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

https://lewissbaker.github.io/2017/11/17/understanding-operator-co-await

https://lewissbaker.github.io/2018/09/05/understanding-the-promise-type

https://github.com/facebook/folly/tree/main/folly/experimental/coro

 

posted @ 2014-10-16 00:58  如果的事  阅读(16036)  评论(2编辑  收藏  举报