精华内容
下载资源
问答
  • c++协程
    千次阅读
    2022-01-16 22:22:19

    协程又称微线程,英文名Coroutine。

    简而言之,协程就是可中断的函数,协程如何实现:保存上下文和恢复上下文。

    一个线程执行一个子程序例如一个函数,调用子程序总是一个入口,一次返回,顺序明确,而协程看上去也是子程序,但执行过程中,在子程序内部可中断,转而执行别的子程序,在适当的时候再返回来执行,有点类似cpu的中断。特点在于是在一个线程执行的。

    协程与多线程的优势?

    1. 协程执行效率极高,没有线程切换的开销,和多线程相比线程数量越多时协程的性能越明显。
    2. 不需要多线程的锁机制,因为只有一个线程。而非线程的抢占式多任务。

    协程是一个线程,那如何利用多核cpu?

            最简单的方法是多进程+协程,能充分利用多核又充分发挥协程效率。

    协程的两个原语:

    1. yeild 让出
    2. resume 恢复

    三种实现协程的方式:

    1. setjmp/longjmp:libmill-基于 setjump/longjump 的协程切换
    2. ucontext :phxrpc-基于 ucontext/Boost.context 的上下文切换
    3. 汇编的代码:libco、Boost.context-基于汇编代码的上下文切换

         基于汇编的上下文切换要比采用系统调用的切换更加高效。

    调度器

    调度器就是处理好协程之间的调用,知道所有协程调用的时机,通过调度器可以实现更多的功能,如定时协程,io协程。

    调度器简单理解为一个队列,将一个协程扔进调度器,调度器根据来执行所有的协程,那么调度器如何执行呢,简单来说就是使用一个循环,从队列中取出协程,然后“复苏”这个协程。

    更多相关内容
  • C++协程

    2022-07-04 10:05:49
    协程是一个可以暂停执行以便稍后恢复的函数。协程是无堆栈的:通过返回给调用者来暂停执行,并且恢复执行所需的数据与堆栈分开存储。这允许异步执行顺序的代码(例如,在没有显式回调的情况下处理非阻塞 I/O),并且...

    协程概念

    协程是一个可以暂停执行以便稍后恢复的函数。

    协程是无堆栈的:通过返回给调用者来暂停执行,并且恢复执行所需的数据与堆栈分开存储。

    这允许异步执行顺序的代码(例如,在没有显式回调的情况下处理非阻塞 I/O),并且还支持惰性计算无限序列和其他用途的算法。

    如果函数的定义执行以下任何一项,则该函数是协程:

    1、使用 co_await 运算符暂停执行直到恢复

    task<> tcp_echo_server()
    {
    	char data[1024];
    	while(true)
    	{
    		size_t n = co_await socket.async_read_some(buffer[data]);
    		co_await async_write(socket,buffer(data,n));
    	}
    }

    2、使用关键字 co_yield 暂停执行返回值

    generator<int> itoa(int n = 0)
    {
    	while(true)
    	{
    		co_yield n++;
    	}
    }

    3、使用关键字 co_return 完成执行返回值

    lazy<int> f()
    {
    	co_return 7;
    }

    每个协程都必须具有满足许多要求的返回类型,如下所述:

    限制

    协程不能使用可变参数、普通返回语句或占位符返回类型(auto 或 Concept)。

    constexpr 函数、构造函数、析构函数和主函数不能是协程。

    执行

    每个协程都与

    a、从协程内部操纵的承诺对象。 协程通过此对象提交其结果或异常。

    b、协程句柄,从协程外部操作。 这是一个非拥有句柄,用于恢复协程的执行或销毁协程框架。

    c、协程状态,它是一个内部的堆分配(除非分配被优化),对象包含

          1)承诺(promise)对象

         2)参数(全部按值复制)

         3)当前暂停点的某种表示,以便 resume 知道在哪里继续, destroy 知道哪些局部变量在范围内

         4)生命周期跨越当前挂起点的局部变量和临时变量

    当协程开始执行时,它会执行以下操作:

    1、使用 operator new 分配协程状态对象(见下文)

    2、将所有函数参数复制到协程状态:移动或复制按值复制参数,按引用参数保持引用(如果在引用对象的生命周期结束后恢复协程,则可能会变得悬空)

    #include <coroutine>
    #include <iostream>
     
    struct promise;
    struct coroutine : std::coroutine_handle<promise>
    { using promise_type = struct promise; };
     
    struct promise {
      coroutine get_return_object()
      { return {coroutine::from_promise(*this)}; }
      std::suspend_always initial_suspend() noexcept { return {}; }
      std::suspend_always final_suspend() noexcept { return {}; }
      void return_void() {}
      void unhandled_exception() {}
    };
     
    struct S {
      int i;
      coroutine f() {
        std::cout << i;
        co_return;
      }
    };
     
    void bad1() {
      coroutine h = S{0}.f();
      // S{0} destroyed
      h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
      h.destroy();
    }
     
    coroutine bad2() {
      S s{0};
      return s.f(); // returned coroutine can't be resumed without committing use after free
    }
     
    void bad3() {
      coroutine h = [i = 0]() -> coroutine { // a lambda that's also a coroutine
        std::cout << i;
        co_return;
      }(); // immediately invoked
      // lambda destroyed
      h.resume(); // uses (anonymous lambda type)::i after free
      h.destroy();
    }
     
    void good() {
      coroutine h = [](int i) -> coroutine { // make i a coroutine parameter
        std::cout << i;
        co_return;
      }(0);
      // lambda destroyed
      h.resume(); // no problem, i has been copied to the coroutine frame as a by-value parameter
      h.destroy();
    }

    3、调用 promise 对象的构造函数。 如果 Promise 类型有一个接受所有协程参数的构造函数,则调用该构造函数,并带有复制后的协程参数。 否则调用默认构造函数。

    4、调用 promise.get_return_object() 并将结果保存在局部变量中。 当协程第一次挂起时,该调用的结果将返回给调用者。 任何在此步骤之前(包括该步骤)引发的异常都会传给调用者,而不是放在 Promise 中。

    5、调用 promise.initial_suspend() 并 co_await 其结果。 典型的 Promise 类型要么返回一个 suspend_always,用于延迟启动的协程,要么返回一个 suspend_never,用于急切启动的协程。

    6、当 co_await promise.initial_suspend() 恢复时,开始执行协程主体。

    7、当协程到达暂停点时,如果需要,在隐式转换为协程的返回类型后,将之前获得的返回对象返回给调用者/恢复者。

    8、当协程到达 co_return 语句时,它会执行以下操作:

    1)调用 promise.return_void() 为

    a、co_return;

    b、co_return expr 其中 expr 的类型为 void

    c、从返回 void 的协程的末尾掉落。 在这种情况下,如果 Promise 类型没有 Promise::return_void() 成员函数,则行为未定义。

    2)或为 co_return expr 调用 promise.return_value(expr),其中 expr 具有非 void 类型

    3)以与创建时相反的顺序销毁所有具有自动存储持续时间的变量。

    4)调用 promise.final_suspend() 并 co_await 其结果。

    9、如果协程以未捕获的异常结束,它将执行以下操作:

    1)捕获异常并从 catch-block 中调用 promise.unhandled_exception()

    2)调用 promise.final_suspend() 并 co_await 结果(例如,恢复协程或获取结果)。 从这一点恢复协程是未定义的行为。

    10、当协程状态因为 co_return 或未捕获的异常终止,或者因为它的句柄被销毁而被销毁时,它会执行以下操作:

    1)调用 promise 对象的析构函数。

    2)调用函数参数副本的析构函数。

    3)调用 operator delete 释放协程状态使用的内存

    4)将执行转移给调用者/resumer。

    堆分配

    协程状态通过非数组 operator new 在堆上分配。

    如果 Promise 类型定义了类级别的替换,则将使用它,否则将使用全局的 operator new。

    如果 Promise 类型定义了一个带有附加参数的 operator new 的放置形式,并且它们匹配一个参数列表,其中第一个参数是请求的大小(类型为 std::size_t),其余的是协程函数参数,这些参数将 传递给 operator new(这使得协程可以使用前导分配器约定)。

    可以优化对 operator new 的调用(即使使用自定义分配器),如果协程状态的生命周期严格嵌套在调用者的生命周期内,并且协程框架的大小在调用站点是已知的。在这种情况下,协程状态嵌入在调用者的堆栈帧中(如果调用者是普通函数)或协程状态(如果调用者是协程)

    如果分配失败,协程抛出 std::bad_alloc,除非 Promise 类型定义了成员函数 Promise::get_return_object_on_allocation_failure()。 如果定义了该成员函数,则分配使用 operator new 的 nothrow 形式,并且在分配失败时,协程立即将从 Promise::get_return_object_on_allocation_failure() 获得的对象返回给调用者。

    Promise

    Promise 类型由编译器使用 std::coroutine_traits 根据协程的返回类型确定。

    形式上,令 R 和 Args... 分别表示协程的返回类型和参数类型列表,Class T 和 /*cv-qual*/(如果有)分别表示协程所属的类类型及其 cv-qualification。如果它被定义为非静态成员函数,它的 Promise 类型由以下决定:

    1、std::coroutine_traits<R, Args...>::promise_type,如果协程没有定义为非静态成员函数,

    2、std::coroutine_traits<R, Class T /*cv-qual*/&, Args...>::promise_type,如果协程被定义为非右值引用限定的非静态成员函数,

    3、std::coroutine_traits<R, Class T /*cv-qual*/&&, Args...>::promise_type,如果协程被定义为右值引用限定的非静态成员函数。

    例如:

    1、如果协程定义为 task<float> foo(std::string x, bool flag);,则其 Promise 类型为 std::coroutine_traits<task<float>, std::string, bool>::promise_type。

    2、如果协程定义为 task<void> my_class::method1(int x) const;,则其 Promise 类型为 std::coroutine_traits<task<void>, const my_class&, int>::promise_type。

    3、如果协程定义为 task<void> my_class::method1(int x) &&;,则其 Promise 类型为 std::coroutine_traits<task<void>, my_class&&, int>::promise_type。

    co_await

    unary operator co_await 挂起协程并将控制权返回给调用者。 它的操作数是一个表达式,其类型必须要么定义 operator co_await,要么可以通过当前协程的 Promise::await_transform 转换为这种类型。

    co_await expr    

    首先,将 expr 转换为可等待对象,如下所示:

    1、如果 expr 由初始挂起点、最终挂起点或 yield 表达式产生,则awaitable是 expr。

    2、否则,如果当前协程的 Promise 类型有成员函数 await_transform,那么 awaitable 就是 promise.await_transform(expr)。

    3、否则,awaitable 就是 expr

    然后,得到awaiter对象,如下:

    1、如果 operator co_await 的重载决议给出了单个最佳重载,则awaiter是该调用的结果(对于成员重载,awaitable.operator co_await(),对于非成员重载,operator co_await(static_cast<Awaitable&&>(awaitable)))

    2、否则,如果重载决议没有找到 operator co_await,则awaiter是可等待的。

    3、否则,如果重载决议不明确,则程序格式错误。

    如果上面的表达式是纯右值,则 awaiter 对象是它的临时物化对象。 否则,如果上面的表达式是一个泛左值,那么 awaiter 对象就是它所引用的对象。

    然后,调用 awaiter.await_ready() (如果知道结果已准备好或可以同步完成,这是避免挂起成本的捷径)。 如果其结果,上下文转换为 bool 为假,则协程被挂起(其协程状态由局部变量和当前挂起点填充)。
    awaiter.await_suspend(handle) 被调用,其中 handle 是表示当前协程的协程句柄。 在该函数内部,挂起的协程状态可以通过该句柄观察到,并且该函数有责任安排它在某个执行程序上恢复或被销毁(返回错误计数作为调度)

    1、如果 await_suspend 返回 void,则立即将控制权返回给当前协程的调用者/恢复者(此协程保持挂起状态)。

    2、如果 await_suspend 返回布尔值,值 true 将控制权返回给当前协程的调用者/恢复者;值 false 恢复当前协程。

    3、如果 await_suspend 返回某个其他协程的协程句柄,则恢复该句柄(通过调用 handle.resume())(注意这可能会链接到最终导致当前协程恢复)

    4、如果 await_suspend 抛出异常,则捕获异常,恢复协程,立即重新抛出异常

    最后,调用 awaiter.await_resume() (无论协程是否挂起),其结果是整个 co_await expr 表达式的结果。

    如果协程在 co_await 表达式中被挂起,然后又被恢复,那么恢复点就在调用 awaiter.await_resume() 之前。

    注意,由于协程在进入 awaiter.await_suspend() 之前已完全挂起,因此该函数可以自由地跨线程传输协程句柄,而无需额外同步。例如,它可以将其放入回调中,计划在异步I/O操作完成时在线程池上运行。在这种情况下,由于当前协同路由可能已经恢复,因此执行了waiter对象的析构函数,当await_suspend()继续在当前线程上执行时,await_suspend()应将*this视为已销毁,并且在句柄发布到其他线程后不访问它。

    Example

    #include <coroutine>
    #include <iostream>
    #include <stdexcept>
    #include <thread>
     
    auto switch_to_new_thread(std::jthread& out) {
      struct awaitable {
        std::jthread* p_out;
        bool await_ready() { return false; }
        void await_suspend(std::coroutine_handle<> h) {
          std::jthread& out = *p_out;
          if (out.joinable())
            throw std::runtime_error("Output jthread parameter not empty");
          out = std::jthread([h] { h.resume(); });
          // Potential undefined behavior: accessing potentially destroyed *this
          // std::cout << "New thread ID: " << p_out->get_id() << '\n';
          std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
        }
        void await_resume() {}
      };
      return awaitable{&out};
    }
     
    struct task{
      struct promise_type {
        task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
      };
    };
     
    task resuming_on_new_thread(std::jthread& out) {
      std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
      co_await switch_to_new_thread(out);
      // awaiter destroyed here
      std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
    }
     
    int main() {
      std::jthread out;
      resuming_on_new_thread(out);
    }

     Possible output:

    Coroutine started on thread: 140193066129216
    New thread ID: 140193045907200
    Coroutine resumed on thread: 140193045907200

    注意:awaiter 对象是协程状态的一部分(作为一个临时对象,其生命周期跨越一个暂停点),并在 co_await 表达式完成之前被销毁。 它可用于根据某些异步 I/O API 的要求维护每个操作的状态,而无需求助于额外的堆分配。

    标准库定义了两个简单的等待项:std::suspend_always 和 std::suspend_never。 

    co_yield

     Yield-expression 向调用者返回一个值并暂停当前协程:它是可恢复生成器函数的通用构建块

    co_yield expr		
    co_yield braced-init-list		

    它相当于

    co_await promise.yield_value(expr)

    典型的生成器的 yield_value 会将其参数存储(复制/移动或仅存储其地址,因为参数的生命周期跨越 co_await 内的暂停点)到生成器对象中并返回 std::suspend_always,将控制权转移给调用者/恢复者。

    #include <coroutine>
    #include <exception>
    #include <iostream>
     
    template<typename T>
    struct Generator { 
      struct promise_type;
      using handle_type = std::coroutine_handle<promise_type>;
     
      struct promise_type { // required
        T value_;
        std::exception_ptr exception_;
     
        Generator get_return_object() {
          return Generator(handle_type::from_promise(*this));
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { exception_ = std::current_exception(); } // saving
                                                                              // exception
     
        template<std::convertible_to<T> From> // C++20 concept
        std::suspend_always yield_value(From &&from) {
          value_ = std::forward<From>(from); // caching the result in promise
          return {};
        }
        void return_void() {}
      };
     
      handle_type h_;
     
      Generator(handle_type h) : h_(h) {}
      ~Generator() { h_.destroy(); }
      explicit operator bool() {
        fill(); 
        /*
           唯一可靠地确定我们是否完成协程,是否将通过 C++ getter(下面的operator())在协程中生成下    
           一个值(co_yield)的唯一方法是执行/恢复协程,直到下一个 co_yield 点(或让它脱落)。然后 
           我们存储/缓存结果以承诺允许getter(下面的operator()在不执行协程的情况下获取它)。
        */
        return !h_.done();
      }
      T operator()() {
        fill();
        full_ = false; // 我们将移出先前缓存的结果以使 promise 再次为空
        return std::move(h_.promise().value_);
      }
     
    private:
      bool full_ = false;
     
      void fill() {
        if (!full_) {
          h_();
          if (h_.promise().exception_)
            std::rethrow_exception(h_.promise().exception_);
            // 在被调用的上下文中传播协程异常
     
          full_ = true;
        }
      }
    };
     
    Generator<uint64_t>
    fibonacci_sequence(unsigned n)
    {
     
      if (n==0)
        co_return;
     
      if (n>94)
        throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow.");
     
      co_yield 0;
     
      if (n==1)
        co_return;
     
      co_yield 1;
     
      if (n==2)
        co_return;
     
      uint64_t a=0;
      uint64_t b=1;
     
      for (unsigned i = 2; i < n; i++)
      {
        uint64_t s=a+b;
        co_yield s;
        a=b;
        b=s;
      }
    }
     
    int main()
    {
      try {
     
        auto gen = fibonacci_sequence(10); // max 94 before uint64_t overflows
     
        for (int j=0; gen; j++)
          std::cout << "fib("<<j <<")=" << gen() << '\n';
     
      }
      catch (const std::exception& ex)
      {
        std::cerr << "Exception: " << ex.what() << '\n';
      }
      catch (...)
      {
        std::cerr << "Unknown exception.\n";
      }
    }

    Output:

    fib(0)=0
    fib(1)=1
    fib(2)=1
    fib(3)=2
    fib(4)=3
    fib(5)=5
    fib(6)=8
    fib(7)=13
    fib(8)=21
    fib(9)=34

    展开全文
  • C++ 协程——实战演示

    2022-06-10 16:01:35
    C++20 添加了一项万众期待的新特性——协程。(在另一篇文章中,我们会谈到 C++20 发布的其他特性;而在先前的文章中,我们已讨论过相关话题:C++ 代码现代化与 C++ 演变。)本篇文章,我们将对 C++ 协程进行一些...

    在这里插入图片描述

    C++20 添加了一项万众期待的新特性——协程。(在另一篇文章中,我们会谈到 C++20 发布的其他特性;而在先前的文章中,我们已讨论过相关话题:C++ 代码现代化与 C++ 演变。)

    本篇文章,我们将对 C++ 协程进行一些实战演示。

    先从一段代码开始。

    template<typename T> 
    unique_generator<T> range(T fromInclusive, T toExclusive) { 
        for (T v = fromInclusive; v < toExclusive; ++v) { 
            co_yield v; 
        } 
    } 
    
    int main() { 
        for (auto val : range(1, 10)) { 
            std::cout << val << '\n'; 
        } 
    } 
    

    可在编辑器浏览器 (Compiler Explorer) 获取上述代码,链接:
    https://coro.godbolt.org/z/zK3E9TEce

    解释一下上述代码。

    协程是一个特殊函数,可暂停执行,随后可在暂停执行的确切位置继续执行。协程执行暂停时,该函数能返回(产生)一个值。协程执行结束时,该函数亦能返回一个值。

    协程暂停时,其状态会复制于代表协程状态的分配对象(不在堆栈中,我们称其为协程“帧”)。协程暂停时,会返回某种“句柄”。返回值本身则由句柄生成。

    上述主体代码中,我们使用“range”作为协程函数。“range”作为协程函数的方法是,使其包含“co_yield”,“co_return”或“co_await”。

    上述函数中使用了“co_yield”,它在保持函数“框架”的同时,会返回一个值,因此我们能在下一次迭代中返回,该函数将保留其状态。

    请注意,这与使用静态变量保留状态不同,因为我们能从不同线程调用或递归调用协程,且每次调用都将独立保留协程自身的“帧”。要想实现这一点,必须将函数状态分配到通过协程返回值管理的“帧”。

    协程返回“句柄”由返回类型设置。该“句柄”持有一个内部 promise_type(请注意,它与 std::promise 无关)。promise_type 必须具有 get_return_object() 函数。promise_type 的其他要求,请参考 cppreference 的协程 promise_type 的相关内容。

    处理 promise_type 及协程“帧”生存期的机制确实是负担。为了避免这种情况,可以使用现有实现,并关注协程本身的实现。cppreference std::coroutine_handle 使用示范为生成器类提供了这样一个实现。上述实例中,我们使用了另一个库中的类似生成器。该库可通过上述 cppreference 链接中生成器的类似方式,为用户提供作为迭代器的 unique_generator 类型(即,可使用返回值类型 unique_generator,迭代协程中产生的值)。

    unique_generator 的用处不容小觑。应由它处理协程帧分配与释放。如需详细了解协程帧处理,请查看 unique_generator 的程序错误修复程序。

    到达 co_return 或函数末尾时,协程将结束执行。在当前示例中,到达循环中的 toExclusive 值后, range 函数将停止运行。

    截至 C++20,部分协程限制:

    协程:

    • 不能使用 return,只能使用 co_return
    • 不能使用可变参数(如:like printf)
    • 不能为常量表达式 (constexpr)
    • 不能为构造函数或析构函数
    • 不能为主体函数
    • 不能使用 auto 或概念作为返回类型(程序员需指定返回类型,以便编译器知道使用何种句柄类型,如 generator;而该类型显然不能通过函数主体内容推断)

    通过引用将参数传递给协程的危害

    来看一个取自 Arthur O’Dwyer 博客的实例:

    unique_generator<char> explode(const std::string& s) { 
        for (char ch : s) { 
            co_yield ch; 
        } 
    }  
    
    int main() { 
        for (char ch : explode("hello world")) { 
            std::cout << ch << '\n'; 
        } 
    } 
    

    上述代码在调用协程函数“explode”时创建了一个临时字符串。然而,因为临时字符串生存期无法扩展为协程帧创建的一部分,该临时字符串将在首次实际使用协程前停止运行。

    正如上述代码中展示的那样,运行地址错误检查器 (-fsanitize=address) 时,可以发现程序错误。没有该 flag,则无法检测到相应程序错误。这意味着这是一个可以在环境中运行,并在生产中崩溃的程序错误。

    请注意,即使试图将临时字符串复制到另一个超过协程生存期的字符串,该问题也无法得到解决:

    unique_generator<char> explode(const std::string& s) { 
        auto ps = std::make_unique<std::string>(s); 
        for (char ch : *ps) { 
            co_yield ch; 
        } 
    } 
    

    因为首次调用协程只是创建,甚至未执行代码主体的第一行,致使上述代码仍然存在未定义行为。随后,首次执行时的临时字符串已然无法运行,那就试着从一个无法运行的临时字符串中创建一个堆分配的字符串(通过调用 make_unique)。请再次注意,运行地址错误检查器 (-fsanitize=address) 时,可发现此示例中的程序错误。而在本例中,如果没有该操作,则无法检测到相应程序错误。

    为了更好地理解创建协程和实际调用协程之间的分离,可将代码主体行一分为二:

    auto coro = explode("hello world"); // (1) coroutine being created 
    for (char ch : coro) {  // (2) coroutine being called 
        std::cout << ch << '\n'; 
    } 
    

    第一行可以标记为 (1),但第二行标记为执行 coro (2),协程中临时字符串(创建于“hello world”)的位置则无法运行。行 (2) 的首次调用可从临时字符串中创建 unique_ptr,然而为时已晚,因为届时临时字符串早已无法运行。

    可通过发送一个非临时字符串变更代码使其生效:

    int main() { 
        std::string s = "hello world"; 
        // may_explode is a coroutine getting const string& 
        for (char ch : may_explode(s)) { // ok doesn't explode now 
            std::cout << ch << '\n'; 
        } 
    } 
    

    然而上述修改仅能改变调用而非函数本身,因此函数仍可临时调用字符串,其间依然存在未定义行为用法。

    可更改函数以期实现比协程更具生存期的效果,例如 unique_ptr:

    unique_generator<char> doesnt_explode(std::unique_ptr<std::string> ps) { 
        for (char ch : *ps) { 
            co_yield ch; 
        } 
    }  
    
    int main() { 
        for (char ch : doesnt_explode(std::make_unique<std::string>("good"))) { 
            std::cout << ch << '\n'; 
        } 
    } 
    

    但是,有人认为上述 API 并不友好。

    也可按值传递字符串,本文将在后续讨论该选项。

    有时运行的代码,取决于参数

    如上所述,如果实际发送的左值引用超过了协程生存期,或者转而发送右值,那么接受常量左值引用的协程则可运行。下列代码正是这种情况,预计通过 std::string_view 实现:

    unique_generator<char> extract(std::string_view s) { 
        for (char ch : s) { 
            co_yield ch; 
        } 
    }  
    
    int main() { 
        // this works ok 
        for (char ch : extract("hello world")) { 
            std::cout << ch << '\n'; 
        } 
     
        // this doesn't 
        using namespace std::string_literals; 
        for (char ch : extract("hello world"s)) { 
            std::cout << ch << '\n'; 
        } 
    } 
    

    同样,未定义行为可通过地址错误检查器 (-fsanitize=address) 显现,而在本代码示例中,如果没有该操作,则无法显现。

    千万不要按值传递参数!

    一些来源(如 SonarSource)建议,涉及协程时,出于安全和避免上述挂起引用场景的考虑,最好按值获取参数。

    我不认同这样的说法。

    第一,按值获取并非总是有效,正如我们在上述string_view示例中看到的那样。(有人认为,视图是一种引用-语义类型,类似于“const T&”,因此按值传递 string_view 实际上并不是“按值”传递。确实如此。然而,从技术层面来说,“按值传递可避免麻烦”的说法并不总是成立。)

    第二,问题不在于我们所期望的参数,而在于发送一个临时参数,这在推出协程之前就是一个已知的问题。

    第三,该过程极其低效,尤其是通过协程实现。

    编写更为通用的协程,以便能从任一容器中或是出于“安全考虑”(持怀疑态度)提取项目,我们将按值获取容器:

    template<typename T> 
    unique_generator<const typename T::value_type&> extract(T s) { 
        for (const auto& val : s) { 
            co_yield val; 
        } 
    } 
    

    请注意,由于协程不支持对其返回类型使用 auto,至少在 C++20 中,我们需要明确表达返回类型。

    在主体代码中,将使用 MyString 类型对象的简单循环同协程循环做比较,作为容器的内部值。因此可以在其构造函数和析构函数中添加打印输出:

    int main() { 
        std::array arr{MyString("Hello"), MyString("World"), MyString("!!!") }; 
        std::cout << "========================\n"; 
        std::cout << "coroutine loop:\n"; 
        std::cout << "------------------------\n"; 
        for (const auto& val : extract(arr)) { 
            std::cout << val << '\n'; 
        } 
        std::cout << "========================\n"; 
        std::cout << "simple loop:\n"; 
        std::cout << "------------------------\n"; 
        for (const auto& val : arr) { 
            std::cout << val << '\n'; 
        } 
    } 
    

    按值获取容器协程的作用可以在打印输出中清楚地看到:

    ======================== 
    coroutine loop: 
    ------------------------ 
    MyString copy ctor: Hello (0x7ffefe1f5790) 
    MyString copy ctor: World (0x7ffefe1f57b0) 
    MyString copy ctor: !!! (0x7ffefe1f57d0) 
    MyString copy ctor: Hello (0x610000000070) 
    MyString copy ctor: World (0x610000000090) 
    MyString copy ctor: !!! (0x6100000000b0) 
    ~MyString: !!! (0x7ffefe1f57d0) 
    ~MyString: World (0x7ffefe1f57b0) 
    ~MyString: Hello (0x7ffefe1f5790) 
    Hello (0x610000000070) 
    World (0x610000000090) 
    !!! (0x6100000000b0) 
    ~MyString: !!! (0x6100000000b0) 
    ~MyString: World (0x610000000090) 
    ~MyString: Hello (0x610000000070) 
    ======================== 
    simple loop: 
    ------------------------ 
    Hello (0x7ffefe1f5710) 
    World (0x7ffefe1f5730) 
    !!! (0x7ffefe1f5750) 
    

    在该示例中,因为发送了生存期超出协程的实际左值引用,我们可以通过引用获取容器。此为变更内容(注意参考 T):

    template<typename T> 
    unique_generator<const typename T::value_type&> extract(const T& s) { 
        for (const auto& val : s) { 
            co_yield val; 
        } 
    } 
    

    现在,对于协程而言,输出将变得更好:

    ======================== 
    coroutine loop: 
    ------------------------ 
    Hello (0x7fff7b224350) 
    World (0x7fff7b224370) 
    !!! (0x7fff7b224390) 
    ======================== 
    simple loop: 
    ------------------------ 
    Hello (0x7fff7b224350) 
    World (0x7fff7b224370) 
    !!! (0x7fff7b224390) 
    

    但是,当前代码仍然允许获取临时代码,这将导致未定义行为:

    for (const auto& val : extract(std::array{MyString("Hi"), MyString("!!")})) { 
        std::cout << val << '\n'; 
    } 
    

    通过输出可以很清楚地发现存在未定义行为,因为我们在析构后打印字符串:

    ======================== 
    coroutine loop: 
    ------------------------ 
    MyString ctor from char*: Hello (0x7ffe650e0fc0) 
    MyString ctor from char*: World (0x7ffe650e0fe0) 
    MyString ctor from char*: !!! (0x7ffe650e1000) 
    ~MyString: !!! (0x7ffe650e1000) 
    ~MyString: World (0x7ffe650e0fe0) 
    ~MyString: Hello (0x7ffe650e0fc0) 
    Hello (0x7ffe650e0fc0) 
    World (0x7ffe650e0fe0) 
    !!! (0x7ffe650e1000) 
    

    同样,代码将随 -fsanitize=address 一起崩溃,且无地址错误检查器。在这种情况下,它将作为一个隐藏的程序错误等待生产。

    我的解决方案是避免挂起引用程序错误,同时实现引用效率,这对协程而言并不新鲜。实施常量引用,删除右值引用:

    void extract(const std::string&& s) = delete; 
    
    unique_generator<char> extract(const std::string& s) { 
        for (char ch : s) { 
            co_yield ch; 
        } 
    } 
    
    int main() { 
        std::string s = "hello world"; 
        for (char ch : extract(s)) { 
            std::cout << ch << '\n'; 
        } 
    
        // doesn't compile! Good!! 
        // for (char ch : extract("temp")) { 
        //     std::cout << ch << '\n'; 
        // } 
    } 
    

    请注意,在这种情况下,上述删除右值版本的想法得以解决未定义行为,但并非无懈可击,且有人认为这是一种不良做法(参考Abseil 第 149 周的提示:对象生存期与= delete,以便就该主题展开有趣讨论)。虽然有争议,也并非无懈可击,但我任然觉得该解决方案很有意义。

    通过协程按顺序遍历二叉树

    该示例受Adi Shavit 在 CppCon 2019 的发言——协程启发。

    假设要按这样的顺序遍历二叉树:

    BinaryTree<int> t1{5, 3, 14, 2, -3, 100, 56, 82, 72, 45}; 
    for (auto val : t1.inorder()) { 
        std::cout << val << '\n'; 
    } 
    

    我们能在二叉树类中实施成员协程函数吗?答案是:是的,我们能!

    请看这里:

    template<typename T> 
    class BinaryTree { 
        struct TreeNode { 
            T value; 
            TreeNode* left = nullptr; 
            TreeNode* right = nullptr; 
            // [...] 
            unique_generator<T> inorder() { 
                if(left) { 
                    for(auto v: left->inorder()) { 
                        co_yield v; 
                    } 
                } 
                co_yield value; 
                if(right) { 
                    for(auto v: right->inorder()) { 
                        co_yield v; 
                    } 
                } 
            } 
        }; 
        TreeNode* head = nullptr; 
        // [...] 
    public: 
        auto inorder() { 
            return head->inorder(); 
        } 
        // [...] 
    }; 
    

    对于一个空二叉树,上述操作会失败,如下所示:

    BinaryTree<int> t2{}; 
    for (auto val : t2.inorder()) { // crashes here, head is null 
        std::cout << val << '\n'; 
    } 
    

    几种有效简单的方法可以解决空树遍历的问题,保持协程方法。请参阅此处。

    总结

    我们已经演示了几个简单协程,特别是生成器协程。协程的主要思想就是向调用对象释放控制时,借助函数保留状态。C++中的协程是极为复杂的程序。协程实现者应管理产生时待创建的帧,但我们使用了一个外部库来管理它。对于临时对象的挂起引用,协程分外敏感,甚至可以说比简单函数还要敏感,就好像我们使用的临时对象活着一样。但是,复制到协程帧的引用则并非如此。如果你听说过按值将对象传递给协程的建议,在高代价的情况下就不会有尝试的想法(这与普通函数调用的建议一致。按值传递要比常量引用更为安全,但对于大型非平凡类型而言,则极为昂贵)。本文讨论了临时引用的危害和避免方法。

    资源与补充材料

    • 协程 (C++20) 来自:cppreference.com
    • CppCon 2019:《生成器、协程与其他智力展开的妙用》 演讲人:Adi Shavit
    • 《C++20 协程教程》 作者:David Mazières
    • 《协程与挂起引用》 作者:Arthur O’Dwyer
    • 《协程实例》 作者:Lewis Baker
    • 《协程与参考参数》 作者:Toby Allsopp
    • 《协程与对象生存期》 作者:Raymond Chen
    • 《通过协程实施 C++ 上下文管理器》

    点击获取试用License!
    在这里插入图片描述

    展开全文
  • 1|0一些实现的c++协程 C++协程实现相关视频讲解:(视频代码资料点击 正在跳转 获取) 协程的实现与原理剖析(上) 协程的实现与原理剖析(下) 协程是一种函数对象,可以设置锚点做暂停,然后再该锚点恢复...

    1|0一些实现的c++协程

    C++协程实现相关视频讲解:(视频代码资料点击 正在跳转 获取)

    协程的实现与原理剖析(上)

    协程的实现与原理剖析(下)

    协程是一种函数对象,可以设置锚点做暂停,然后再该锚点恢复继续运行,我觉得这是最合适的定义,用户态线程,轻量级线程,可中断恢复的函数,这些都不够精确,先来认识一个boost 1.75的一个例子

    #include <iostream>
    #include <boost/coroutine2/all.hpp>
    
    void coroutine_function(boost::coroutines2::coroutine<void>::pull_type & coro_back)
    {
        std::cout << "a ";
        coro_back(); // 锚点,返回
        std::cout << "b ";
        coro_back(); //锚点 返回
        std::cout << "c ";
    }
    
    int main()
    {
        boost::coroutines2::coroutine<void>::push_type coroutine_object(coroutine_function); 	// 创建协程
        std::cout << "1 ";
        coroutine_object(); // 运行协程
        std::cout << "2 ";
        coroutine_object(); // 返回锚点,继续运行协程
        std::cout << "3 ";
        coroutine_object(); // 返回锚点,继续运行协程
        return 0;
    }
    
    g++ test.cpp -lboost_coroutine -lboost_context -o test
    ./pull
    --------------输出分割线-------------
    1 a 2 b 3 c 

    在main( )中创建了一个协程 coroutine_object,然后调用coroutine_object()去运行,实际上运行的coroutine_function( )函数,而且每次运行到coro_back();就中断当前的执行返回,下次调用coroutine_object()就从这个断点继续运行,这就是协程的全部了

    为什么会有协程是轻量级线程的说法呢?因为协程具有中断可恢复的特性,那么只需要在开一个全局的数组存储所有的协程,在协程中断时,不断轮转调用下一个协程继续运行即可; 这看起来似乎和线程无异,但其实有巨大的区别,因为协程本质是函数,调用协程后原来的地方就会被阻塞,协程处理完了才返回结果,这是天然同步的,而多线程无法做到这点,因为多线程的调度受内核控制,触发点来自于硬件时钟中断不可预见,同时又运行在多核心下,调用后运行次序是不确定的,想实现同步调用就必须通过std::promise/future 去辅佐,但为了性能往往见到的是异步+回调的方式进行多线程的交互,异步回调代码的可读性是很差的而且还需要考虑一大堆并发上锁的情况,协程因其函数本质,是天然同步的,而在遇到阻塞条件时候,把cpu让给别的协程,等条件满足了再通过中断可恢复的特性再继续运行,就实现了并发,同步+并发就是协程强大的地方,其使用范式和轮转+同步非阻塞很像

    接下来会介绍一些目前的实现的协程,有非官方的: boost.coroutine2的协程,使用起来方便,让我们可以直观了解协程;微信的libco, 源码很好阅读,资料多,可以进一步学习到协程是如何实现运行的;而官方本身的c++20协程,还不成熟,使用起来比较复杂,官方的东西还是需要提前了解;

    C/C++ Linux服务器开发高级架构学习视频点击观看:C/C++Linux服务器开发/Linux后台架构师-学习视频

    2|0一些实现的c++协程

    2|1boost中的协程

    push_type和pull_type

    boost自己早就实现了一套协程,先后推出了两个版本boost coroutine和boost coroutine2,现在第一个版本boost coroutine已经弃用, 直接看看coroutin2的简单例子

    #include <iostream>
    #include <boost/coroutine2/all.hpp>
    
    void foo(boost::coroutines2::coroutine<int>::push_type & sink)
    {
        std::cout<<"start coroutine\n";
        sink(1);
        std::cout<<"finish coroutine\n";
    }
    
    
    int main()
    {
        boost::coroutines2::coroutine<int>::pull_type source(foo);
        std::cout<<source.get()<<std::endl;
        std::cout<<source()<<std::endl;
        std::cout<<"finish\n";
        return 0;
    }

    编译链接运行后

    g++ pull.cpp -lboost_coroutine -lboost_context -o pull
    ./pull
    --------------输出分割线-------------
    start coroutine
    1
    finish coroutine
    finish

    boost.corountine2中的协程增加了push_typepull_type用于提供协程数据的流转,约束了数据的从push_type流入,从pull_type流出, 上面的demo定义协程对象source的时候使用了pull_type,所以协程函数参数类型是push_type

     

    当协程对象被创建之后就直接运行,直到sink(1)的时候暂停返回到main中,main中使用source.get()获取数据,继续使用source()调用协程对象,协程从sink(1)之后继续运行执行完毕,返回main,main也执行完毕。

    上面是一个pull的例子,接下来是一个push的例子

    #include <iostream>
    #include <boost/coroutine2/all.hpp>
    
    void foo(boost::coroutines2::coroutine<int>::pull_type& sink)
    {
        std::cout<<"start coroutine\n";
        //sink();
        int a = sink().get();
        std::cout<<a<<std::endl;
        std::cout<<"finish coroutine\n";
    }
    
    
    int main()
    {
        boost::coroutines2::coroutine<int>::push_type source(foo);
        
        std::cout<<"finish\n";
        source(0);
        source(5);
        return 0;
    }

    编译输出

    g++ push.cpp -lboost_coroutine -lboost_context
    ./push 
    --------------输出分割线-------------
    finish
    start coroutine
    5
    finish coroutine

    也可以看到一个细节, 当source为pull_type的时候,协程是马上运行的,因为此时不用传递数据进行,而push_type的时候,需要source()才会运行,第一次需要放一个没用的数据


    为了使用方便,boost::coroutine2实现了协程迭代器,如下

    template< typename T >
    class push_routine{
        ....
        
    
        push_coroutine< T > &
        push_coroutine< T >::operator()( T const& t) { //() 切换协程
            cb_->resume( t);
            return * this;
        }
        
        
        class iterator{	// 实现迭代器
            ....
            
            iterator & operator++() noexcept {
                return *this;
            }
           
        }
           
    }

    所以其支持如下用法,直接在range-for切换元素的时候就能恢复运行

        boost::coroutines2::coroutine<void>::push_type source(foo);
        for(auto& s : source){
        	std::cout<<"run"
        }

    关于C/C++ Linux后端开发网络底层原理知识 点击 学习资料 获取,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。

    fiber

    因为push_type和pull_type这样的简洁组合已经可以解决基本问题---同步调用的中断恢复,但是只有多协程并发才能发挥其真正威力,为此需要同步和调度,boost搞了个fiber(纤程,这才是轻量级线程)出来,是在coroutine2的基础上添加了协程调度器以及barrier mutex channel promise future condition_variable, sleep yield 等协程同步工具,这些和线程同步工具很像,因为在多协程场景下,它两模型和解决的问题都是一样的,都是通过调度多实体实现并发,但是协程有很多好处,开销很小,而且调度是运行的协程自己控制让出cpu给下一个要运行的线程,是可预见的,同时调用上是同步的,保证了顺序性就可以避免锁,

    下面是boost的fiber的一个例子

    
    #include <boost/fiber/all.hpp>
    #include <iostream>
    
    using namespace std;
    using namespace boost;
     
    void callMe(fibers::buffered_channel<string>& pipe) {
        pipe.push("hello world");
    }
     
     
    int main() {
        fibers::buffered_channel<string> pipe(2);
        fibers::fiber f([&]() {callMe(pipe); });
        f.detach();
        string str;
        std::cout<<"start pop"<<std::endl;
        pipe.pop(str); //切换协程运行
        std::cout<<"get str:"<<str<<std::endl;
        return 0;
    }

    编译运行

    g++ channel.cpp -o channel -lboost_fiber -lboost_context
    ./channel 
    -------------------输出分割线-------------------
    start pop
    get str:hello world

    这是一个最简单的例子,并没有去体现使用一个loop去做调度协程,调度还是由一些函数手动触发的

    注意pull_type和push_type的操作已经没有了,那协程是如何切换的呢? 切换发生在pipe.pop( )中, fibers::buffered_channel是一个缓存队列,用来传输数据,pop的底层检测到没有数据,会就开始让出cpu,底层的协程调度器就开始调度别的协程进行运行,没有看过源码不知道执行到pipe.push的时候是否有没有发生调度,也许有也许没有,但都不太重要,因为这就和线程是一样的;

    由于fiber中有调度器的存在,当前协程主动让出cpu,调度器让别的协程运行,比如上面的pipe.pop(),相当执行了一个协程的co_yield()操作让出cpu;所以,某个协程中如果有阻塞操作,将导致整个线程都处于阻塞,所有协程都被阻塞, 此文提出两种解决方法

    同步改成非阻塞,一旦发现未达到条件直接yield()让出cpu,再后面轮转调度还能回到该店

    int read_chunk( NonblockingAPI & api, std::string & data, std::size_t desired) {
        int error;
        while ( EWOULDBLOCK == ( error = api.read( data, desired) ) ) {
            boost::this_fiber::yield();
        }
        return error;
    }

    同步操作改成异步操作,使用协程级的future和promise进行等待转让cpu给别的协程

    std::pair< AsyncAPI::errorcode, std::string > read_ec( AsyncAPI & api) {
        typedef std::pair< AsyncAPI::errorcode, std::string > result_pair;
        boost::fibers::promise< result_pair > promise;
        boost::fibers::future< result_pair > future( promise.get_future() );
        // We promise that both 'promise' and 'future' will survive until our lambda has been called.
        // Need C++14
        api.init_read([promise=std::move( promise)]( AsyncAPI::errorcode ec, std::string const& data) mutable {
                                promise.set_value( result_pair( ec, data) );
                      });
        return future.get();
    }

    2|2asio中的协程

    asio的协程总感觉有两个版本,一个是c++20之前就有的版本,还有一个是在c++20的提供的协程的基础上封装的版本;asio的协程是无栈协程(后文会介绍),无栈协程除了运行高效,节省内存之外,还能通过gdb查看到调用堆栈,有栈协程的堆栈因为被汇编切换走了是没法看到的,asio基于其io_context(详见asio的异步与线程模型解析)实现了多协程调度,所以要使用它的协程就就需要用到它的io_context(在此可以理解成一个跑着loop的协程调度器),该例子取自asio/src/examples/cpp17/coroutines_ts/echo_server.cpp

    #include <asio/co_spawn.hpp>
    #include <asio/detached.hpp>
    #include <asio/io_context.hpp>
    #include <asio/ip/tcp.hpp>
    #include <asio/signal_set.hpp>
    #include <asio/write.hpp>
    #include <cstdio>
    #include <iostream>
    
    using asio::ip::tcp;
    using asio::awaitable;
    using asio::co_spawn;
    using asio::detached;
    using asio::use_awaitable;
    namespace this_coro = asio::this_coro;
    
    #if defined(ASIO_ENABLE_HANDLER_TRACKING)
    # define use_awaitable \
      asio::use_awaitable_t(__FILE__, __LINE__, __PRETTY_FUNCTION__)
    #endif
    
    awaitable<void> echo(tcp::socket socket)
    {
      try
      {
        char data[1024];
        for (;;)
        {
          std::size_t n = co_await socket.async_read_some(asio::buffer(data), use_awaitable);
          co_await async_write(socket, asio::buffer(data, n), use_awaitable);
        }
      }
      catch (std::exception& e)
      {
        std::printf("echo Exception: %s\n", e.what());
      }
    }
    void fn2(){
        std::cout<<"hhh\n";
    }
    
    void fn(){
        fn2();
    }
    
    awaitable<void> listener()
    {
      auto executor = co_await this_coro::executor;
      fn(); 
      tcp::acceptor acceptor(executor, {tcp::v4(), 8988});
      for (;;)
      {
        tcp::socket socket = co_await acceptor.async_accept(use_awaitable); //调用协程,体现同步性
        co_spawn(executor, echo(std::move(socket)), detached);// 创建连接处理线程
      }
    }
    
    int main()
    {
      try
      {
        asio::io_context io_context(1);
    
        asio::signal_set signals(io_context, SIGINT, SIGTERM);
        signals.async_wait([&](auto, auto){ io_context.stop(); });
    
        co_spawn(io_context, listener(), detached); // 创建纤程,体现并发性
    
        io_context.run();							// 开始调度
      }
      catch (std::exception& e)
      {
        std::printf("Exception: %s\n", e.what());
      }
    }
    

    代码很长,但只需要看main( )就可以了,co_spawn( )创建了一个协程,然后使用io_context.run( ),对基于该io_context创建的协程进行调度, 上面实现的协程函数listener( )中,使用co_await acceptor.async_accept(use_awaitable)做一个协程的阻塞同步调用,async_accept( )中发现没有新的连接就让出cpu给当前io_context下别的协程继续运行,当时间片又切回到该协程时,发现有新的链接时候,往io_context中创建一个新的协程去处理该连接,这里就能很好的体现了协程的同步和并发的应用场景,调度过程;

    asio的协程是基于c++20实现的,简单的介绍因为asio库很通用,还没有精力继续研究,但可以先来看看c++20的协程给的基础设施。

    2|3c++20的协程

    c++20的协程目前只是一套框架基础,远未成熟,最好的文档参考还是cppreference,同时的这里两篇很好的文章进行了介绍文章1和文章2

    先看一个非常简化的例子看整体

    #include <iostream>
    #include <thread>
    #include <coroutine>
    #include <future>
    #include <chrono>
    #include <functional>
    
    struct Result{
      struct promise_type {
        Result get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
      };
    };
    
    std::coroutine_handle<> coroutine_handle;
    
    struct AWaitableObject
    {
    	AWaitableObject() {}
        
    	bool await_ready() const {return false;}
    
    	int await_resume() { return 0; }
    
    	void await_suspend(std::coroutine_handle<> handle){
            coroutine_handle = handle;
        }
    
    };
    
    
    Result CoroutineFunction()
    {
        std::cout<<"start coroutine\n";
    	int ret = co_await AWaitableObject(); 
        std::cout<<"finish coroutine\n";
    }
    
    
    
    int main()
    {
        std::cout<<"start \n"; 
        auto coro = CoroutineFunction();
        std::cout<<"coroutine co_await\n"; 
        coroutine_handle.resume();
    
        return 0;
    }

    对该程序使用如下方式进行编译运行(需g++10.2.0及以上)

    g++ test4.cpp -O0 -g -o test4 -fcoroutines -std=c++20
    start 
    start coroutine
    coroutine co_await
    finish coroutine

    我们可以看到它的运行正如一般协程一样, 在使用了关键字co_await后会返回到caller, 在main中使用resume()后,回到co_await的赋值等式中运行

     

    接下来,介绍目前c++协程的设计思想和细节

    Results CoroutineFunction(){
    	
    	co_await AwaitatbleObject();
    	
    	co_return {};
    }
    

    一个协程函数形式如上,当函数体内出现了co_await, co_yield,co_return这三个关键字之后,就会被当成一个协程函数;此时,编译器要求返回值类型是否包含一个promise_type的结构以及需要实现必要的函数,以上一个例子中的Result类型为例:

    struct Result{
      struct promise_type {
        Result get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void unhandled_exception() {}
        
        suspend_aways yield_value(){} // 对应co_yield
        void return_void() {}	//对应co_return
        Result return_value(const Result& res){ return res;}
      };
    };

    c++20的编译器对于协程的运行有一套流程,我们可以通过提供promise_type去控制这个流程,同时对于协程的caller而言,协程运行后的只能获得返回值,所以希望通过它与协程进行后续交互的主要对象, 获取返回值, 处理异常等功能,所以这个很重要的控制器struct promise_type就放在了返回值类型Result中;

    下面介绍promise_type的接口在协程运行如何交互,从头到尾,主要分成下面三个阶段

    开头初始化准备:

    • 协程函数运行后,首先生成一个promise_type对象
    • 调用get_return_object()函数创建返回值对象,这个对象会在协程第一次返回时就会把这个对象返回给caller;
    • 调用initial_suspend()函数,这个返回值有两个选择suspend_never/suspend_always,never表示继续运行,always表示协程挂起,同时把返回值对象返回,所以这个接口的语义是,协程创建后是否马上运行

    运行:

    • 开始运行协程函数,如果出现异常会调用unhandled_exception()去处理
    • 如果遇到co_yield var这样的表达式,表示想要挂起当前协程,返回一个值给caller店, 编译器调用yield_value(var)方法,我们可以此时将值设置到Result的相关变量中,编译器会继续根据函数的返回值判断是否为suspend_always判断要返回到caller点
    • 如果co_return这样的表达式,想要结束协程返回一个对象,则会调用return_value()这个函数,设置好要返回的相关值; 如果整个协程都没有出现co_return,则会调用return_void()

    结束善后:

    • 最后调用final_suspend() 判断协程已处理完毕释放前是否要挂起

    其中有一个重要的关键字--co_await, 这是一个一元操作符,操作的对象为awaitable类型,就是实现await_ready(), await_resume(), await_suspend( ) 的类型,如例子所示的AWaitableObject

    struct AWaitableObject
    {
    	AWaitableObject() {}
        
    	bool await_ready() const {return false;}
    
    	int await_resume() { return 0; }
    
    	void await_suspend(std::coroutine_handle<> handle){
            coroutine_handle = handle;
        }
    };

    当使用co_await awaitable_object时:

    • 首先运行await_ready( )函数,判断是否要挂起当前线程: 如果是false,则不挂起; 如果是true,则表示要挂起,然后会调用await_suspend(),用于提供挂起前的处理,然后协程就被挂在这个点
    • 一旦协程被恢复运行时,继续调用await_resume()在返回一个值到协程挂起点,如例子所示

     

    co_await除了显示使用之外,promise_type的接口中凡是返回了suspend_never/suspend_always的地方,编译器都是通过co_await的方式调用这些函数的,suspend_never/suspend_always是awaitable类型

    struct suspend_always
      {
        bool await_ready() { return false; }
    
        void await_suspend(coroutine_handle<>) {}
    
        void await_resume() {}
      };
    
      struct suspend_never
      {
        bool await_ready() { return true; }
    
        void await_suspend(coroutine_handle<>) {}
    
        void await_resume() {}
      };

    每个协程都对应一个handle,用来管理协程的挂起和恢复,比如说handle.resume()就是用来恢复协程的运行的

    协程handle的获取有两种方式:

    • 第一种是通过co_await的await_suspend( )方法,该方法被调用时就能拿到协程的handle,但是这个方法肯定是不太好;
    • 另一种方法是可以从promise_type对象中拿到,需要使用这个方法coroutine_handle<promise_type>::from_promise(promise_type obj)基于此,我们可以对返回值做如下改造
    struct Result{
      //add
      Result(promise_type* obj):promise_type_ptr(obj){}
      //add
      void resume(){
      	promise_type_ptr->resume();
      }
    
      struct promise_type {
        // mod
        Result get_return_object() { 
        	return Reuslt(this);
       	}
       	
       	// add
       	void resume(){
       		coroutine_handle<promise_type>::from_promise(*this).resume();
       	}
       	
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void unhandled_exception() {}
        
        suspend_aways yield_value(){}
        void return_void() {}	
        Result return_value(const Result& res){ return res;}
      };
      
      // add
      promise_type *promise_type_ptr;
    };
    

    则可以通过如下方式使用

    auto result = CoroutineFunction();
    result.resume();

    从promise_type到awaitable object,c++20的协程目前提供的更多的是一个灵活的基础框架,离使用上还有一段距离


    除此之外还有大量的优秀的协程库,比如基于c++20的libcopp, cppcoro,以及不依赖微信自己实现的libco(由于篇幅原因,libco介绍与实现分析不放在当前文章)

    3|0协程的一些应用场景

    awaitable<void> listener()
    {
      auto executor = co_await this_coro::executor;
      fn(); 
      tcp::acceptor acceptor(executor, {tcp::v4(), 8988});
      for (;;)
      {
        tcp::socket socket = co_await acceptor.async_accept(use_awaitable); //调用协程,体现同步性
        co_spawn(executor, echo(std::move(socket)), detached);// 创建连接处理线程
      }
    }
    
    int main()
    {
      try
      {
        asio::io_context io_context(1);
    
        asio::signal_set signals(io_context, SIGINT, SIGTERM);
        signals.async_wait([&](auto, auto){ io_context.stop(); });
    
        co_spawn(io_context, listener(), detached); // 创建协程,体现并发性
    
        io_context.run();							// 开始调度
      }
      catch (std::exception& e)
      {
        std::printf("Exception: %s\n", e.what());
      }
    }

    在asio的例子中很好的介绍了协程的使用方式了,主要是不断的创建协程,让调度器调度运行,在协程运行过程对于一些会阻塞的条件,做一个非阻塞的检测中,发现条件不满足就让出cpu,这就是常见轮转+非阻塞同步。

    4|0协程的分类

    4|1有栈协程和无栈协程

    协程可以分成有栈stackful和无栈stackless两种,比如,libco就是有栈协程, 每个协程创建的时候都会获得一块128k的堆内存,协程运行的时候就是使用这块堆内存当作运行栈使用,切换时候保存/恢复运行栈和相应寄存器,而无栈协程不需要这些,因为无栈协程的实现原理并不是通过切换时保存/恢复运行栈和寄存器实现的,它的实现见下,由于协程的每个中断点都是确定,那其实只需要将函数的代码再进行细分,保存好局部变量,做好调用过程的状态变化, 下面就将一个协程函数fn进行切分后变成一个Struct,这样的实现相对于有栈协程而言使用的内存更少,因为有栈协程的运行栈由堆获得,必须要保证运行栈充足,然而很多时候用不到这么多的内存,会造成内存浪费;

    void fn(){
    	int a, b, c;
    	a = b + c;
    	yield();
    	b = c + a;
    	yield();
    	c = a + b;
    }
    
    ----------------------------分割线---------------------------------
    Struct fn{
    	int a, b, c;
    	int __state = 0;
    	
    	void resume(){
    		switch(__state) {
            case 0:
                 return fn1();
            case 1:
                 return fn2();
            case 2:
            	 return fn3();
            }
    	}
    	
    	void fn1(){
    		a = b + c;
    	}
    	
    	void fn2(){
    		b = c + a;
    	}
    	
    	void fn3(){
    		c = a + b;
    	}
    };

    4|2对称和非对称

    boost.coroutine2和libco这类属于非对称协程,这类协程的特点是存在调用链,有调用和返回的关系,比如说coroutine2中进行source()的时候去调用协程了,协程执行到阻塞点sink()返回,而不是让出cpu,随便执行别的协程;

    展开全文
  • C++ 协程

    千次阅读 2020-09-04 09:58:44
    现有的 C++ 协程库均基于两种方案:利用汇编代码控制协程上下文的切换,以及利用操作系统提供的 API 来实现协程上下文切换。典型的例如: libco,Boost.context:基于汇编代码的上下文切换 phxrpc:基于 ucontext/...
  • C++协程线程

    千次阅读 2022-03-28 13:41:23
    c++20协程几个基本概念: 可恢复类型 用于恢复协程类型,对应协程就是协程返回类型,调用者可通过它恢复协程,也可通过内部承诺型拿到协程返回值. 承诺类型 主要用来保存协程值,如果需要恢复协程时,需要通过承诺类型拿到...
  • c++协程(todo)

    千次阅读 2021-11-29 23:35:12
    什么是协程 传统的C++服务器框架,一般是同步I/O或者异步I/O。 同步 I/O 框架 同步I/O框架往往使用的是多进程模式:一个父进程负责 accept 传入连接,然后 fork 一个子进程处理;或者是一个父进程创建了一个 socket ...
  • c++协程实现的原理和机制

    千次阅读 2021-07-13 18:46:14
    一、协程 多线程编程对程序员来说绝对是一场噩梦,特别在大规模的高并发服务端编程中,线程池和异步IO共同工作,...在前边的《c++20中的协程》中,对c++20中STL库提供的无栈协程进行了初步的分析和应用。其它如libco等
  • C++协程库实现

    2021-03-15 20:35:06
    协程,是一个程序组件,其功能其实就是执行一段可由用户随时中断或继续运行的代码,可与异步机制结合。一个线程中,可存在多个协程。 实现 windows平台下具有Fiber概念,其API提供了创建CreateFiberEx、删除...
  • 我想在ibfiber的基础上实现一个简单的协程池类。我希望它是单例模式的,并且是线程安全的。 执行的任务由std::function加入到任务队列里。 协程池类的实现 可以看到,下面的协程池类主要是三个接口:getInstance()、...
  • libco ...协程开发,避免了多线程中的奇葩错误(多线程错误不好找原因,而且新手很容易出现),go语言、python、js、中都有协程的开发...协程分为对称协程(symmetric)和非对称协程(asymmetric),对称协程需要显式指定
  • C++协程线程池

    千次阅读 2022-03-28 16:54:24
    #协程线程池 如何等待协程完成 协程表示懒任务,完成任务时恢复协程,从而...初始挂起,由于返回从不挂起,不会挂起协程,执行协程函数体(函数体),然后执行协待异步读文件{"../主.c++"},协待式会产生3个结果: 1,挂起当前协程
  • 详解C/C++协程实现原理及使用

    千次阅读 2020-12-18 18:43:28
    协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A...
  • 前一篇博客介绍了libfiber里使用协程的简单方法,这里介绍下调度协程的三种方式 自动调度,立即执行 这种模式下,创建的协程会被立即执行,协程结束后才返回创建处的代码逻辑。 void schedule_two(void) { acl::...
  • C/C++ 协程的实现

    2021-02-05 17:58:31
    https://github.com/cloudwu/coroutine https://zhengyinyong.com/post/ucontext-usage-and-coroutine/ https://www.zhihu.com/column/p/31176003 https://www.jianshu.com/p/a96b31da3ab0
  • C++协程入门

    2020-05-08 16:53:05
    2 协程的实现 协程在一些脚本语言如Python、Lua中都已经很好地支持了(C++20也支持协程),但为了更好地学习它,还是有必要去逐步封装一个协程。本文主要利用Linux的ucontext库去封装一个Coroutine类,再与boost的...
  • C++ 协程协程介绍

    2020-08-14 08:40:40
    文章目录产生背景协程和异步 IO以及多线程的对比处理 IO 密集型任务协程的实现机制协程函数的特点有栈协程无栈协程有栈协程和无栈协程对比 产生背景 对于后台开发,我们有一个非常重要的问题即使用尽可能少的服务器...
  • 最近研究高性能C++协程,网上了解到了魅族libgo、腾讯libco、开源libaco、boost coroutine,这里记录一下。 1 什么是协程 协程可以很轻量的在子例程中进行切换,它由程序员进行子例程的调度(即切换)而不像线程那样...
  • c++协程库实现-day2

    2022-04-19 18:07:22
    B站:毛毛and西西 视频 : c++携程库,第2部分 协程框架已开源到github和gitee上 gitee地址:https://gitee.com/meiqizhang/xfiber github地址:https://github.com/meiqizhang/xfiber.git xfiber.cpp #include "xfiber....
  • Libgo is a stackful coroutine library for collaborative scheduling written in C++ 11, and it is also a powerful and easy-to-use parallel programming library. Three platforms are currently supported: ...
  • 作者:peterfan,腾讯 WXG 客户端开发工程师背景基于跨平台考虑,微信终端很多基础组件使用 C++ 编写,随着业务越来越复杂,传统异步编程模型已经无法满足业务需要。Modern C...
  • Coroutinecc 协程库 基于*nix的实现 参考云风的实现 支持linux、unix、Mac OS X 协程库实现相关文章可以参考 未来改进: 提供对等协程实现 hook进一些systemcall,将所有fd设为非阻塞,并在epoll中切换协程,实现...
  • 由于GIL的存在,导致Python多线程性能甚至比单线程更糟。GIL: 全局解释器锁(英语:Global Interpreter Lock,缩写GIL),是计算机程序设计语言解释器用于同步线程的一种机制,...协程: 协程,又称微线程,纤程,英...
  • 协程,又称微线程,纤程。英文名Coroutine。 协程的概念很早就提出来了,但直到最近几年才在某些语言(如Lua)中得到广泛应用。 子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又...
  • 时隔三个月,才回到当时说的学协程的坑,中间学了各种各样的东西,起码对现代C++ 有些许了解了。尾递归优化 快速排序优化 CPS 变换 call/cc setjmp/longjmp coroutine 协程 栈编程和控制流 讲解_我说我谁呢 --CSDN...
  • c++协程初探

    2020-09-08 19:22:42
    介绍协程之前,先来复习一下进程和线程。 进程:应用程序的启动实例,是计算机资源分配的最小单位,进程拥有代码和打开的文件资源、数据资源、独立的内存空间。 线程:从属于进程,是程序的实际执行者。是计算机...
  • 本文翻译自c++协程库cppcoro库作者Lewis Baker的github post,本篇为第三篇,原文内容在...这是C++协程文章系列的第三篇。 The previous articles in this series co

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,623
精华内容 5,449
关键字:

c++协程