精华内容
下载资源
问答
  • 主要介绍了详细解读tornado协程(coroutine)原理,涉及协程定义,生成器和yield语义,Future对象,ioloop对象,函数装饰器coroutine等相关内容,具有一定借鉴价值,需要的朋友可以参考下
  • 项目名称PHPCE,全名PHP-Coroutine-Engine 本项目是php7的分支,主要功能是在php7的基础上实现协程。 这个项目是从php官方的github中fork出来的版本,基于php7.1.17版本。 协程是一种可以支持高并发服务器的设计模式...
  • boost.coroutine2 boost.coroutine2提供了用于通用子例程的模板,该模板允许多个入口点在某些位置挂起和恢复执行。 它保留了执行的本地状态,并允许多次输入子例程(如果必须在函数调用之间保持状态,则很有用)。 ...
  • 使用Posix线程实现的coroutine 协程的关键在于栈的保存沿用,有很多其他版本的C实现的coroutine,如:setcontex, setjmp/longjmp。我认为线程拥有自己的数据栈,天然提供栈的沿用,再利用pthread_mutex_t, pthread_...
  • coroutine-dialog-fragment:带有Kotlin coroutine的DialogFragment
  • 라이브러리설정 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' 개요 코루틴이란지무엇인다。 。이점을다。 키워드暂停키워드사키워드다。 코루틴이실행되는스레드의알아본알아본다...
  • 主要介绍了Lua的协程(coroutine)简介,本文讲解了coroutine的创建、协程的三种状态和yield函数的配合使用等内容,需要的朋友可以参考下
  • 主要介绍了c++支持coroutine的简单示例,使用的是linux平台做的,需要的朋友可以参考下
  • 主要为大家详细介绍了C++ Coroutine的简单学习教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • 协程 使用线程在 C++11 中实现简单的“协程”
  • Python coroutine

    2018-12-01 15:27:46
    David Beazley作的关于Python并发coroutine的介绍slides
  • coroutine 与 Future 的关系 看起来两者是一样的,因为都可以用以下的语法来异步获取结果, result = await future result = await coroutine 实际上,coroutine 是生成器函数,它既可以从外部接受参数,也可以...
  • let coro: Coroutine < i32> = Coroutine :: spawn ( | me,_ | { for num in 0 .. 10 { me. yield_with (num); } usize :: MAX }); for num in coro { println! ( "{}" , num. unwrap ()); } }
  • 主要介绍了Lua协程(coroutine)程序运行分析,本文讲解分析了一段lua 协程代码是如何运行的,需要的朋友可以参考下
  • 协程的简单实现 ...coroutine.h 协程头文件 coroutine.c 协程实现 test_simple.c 简单测试 test_socket.c 非阻塞socket测试 client.py 单线程socket,用户输入测试 client_thread.py 多线程socket测试
  • LUA - coroutine

    2014-09-16 11:40:05
    The concept of a coroutine is one of the oldest proposals for a general control abstraction. It is attributed to Conway [Conway, 1963], who described coroutines as “subroutines who act as the master ...
  • NULL 博文链接:https://dsqiu.iteye.com/blog/2029701
  • Coroutine是基于Kilim/Promise JDeferred的协程式驱动框架,基于Apache Zookeeper的分布式规则存储和动态规则变更通知。 主要特性: 1. 基于微服务框架理念设计 2. 支持同步/异步调用 3. 支持串行/并行调用 4....
  • 协程Coroutine和Kilim

    2019-04-15 01:06:17
    NULL 博文链接:https://eleopard.iteye.com/blog/1750384
  • 添加支持从Bluebird.coroutine()产生ES6生成器,迭代器,数组和对象的简单函数 安装 npm install bluebird-yield 用法 // At the begging of you project add a yield handler const Bluebird = require ( '...
  • loco(log coroutine)是一个在android上使用coroutine的日志库。
  • C++20 协程coroutine

    2021-08-04 22:23:18
    然后检查struct std::coroutine_traits::promise_type是否满足如下描述的coroutine接口: coroutine接口要求具有public promise_type结构,而promise_type中必须有get_return_object,initial_suspend,final_...

    1.协程概念

    协程函数与普通函数的区别:

    (1)普通函数执行完返回,则结束。
    协程函数可以运行到一半,返回并保留上下文;下次唤醒时恢复上下文,可以接着执行。

    协程与多线程:

    (1)协程适合IO密集型程序,一个线程可以调度执行成千上万的协程,IO事件不会阻塞线程
    (2)多线程适合CPU密集型场景,每个线程都负责cpu计算,cpu得到充分利用

    协程与异步:

    (1)都是不阻塞线程的编程方式,但是协程是用同步的方式编程、实现异步的目的,比较适合代码编写、阅读和理解
    (2)异步编程通常使用callback函数实现,将一个功能拆分到不同的函数,相比协程编写和理解的成本更高。
    (3)个人觉得异步编程都可以改成协程。

    2.关键字

    C++20引入了三个新的关键字 co_await, co_yield, co_return实现协程。包含这三个关键字的函数就是协程函数。

    co_await :用来暂停和恢复协程的, 返回awaitable类型.
    co_yield :用来暂停协程并且往绑定的 promise类型 里面 yield 一个值.
    co_return :往绑定的 promise类型 里面放入一个值.

    这里提到了2个概念:promise和awaitable。这2个概念比较重要,理解这2个概念后才能定制协程的流程和功能。

    3.promise和awaitable类型

    (1)promise和awaitable类型都是“鸭子类型”,不是继承具体的基类,而是实现了各自要求的api接口的就是promise类型或awaitable类型

    (2)最简单的awaitable类型需要实现3个函数:
    1)await_ready:返回类型bool,表示awaitable实例是否已经ready。协程开始会调用此函数,如果返回true,表示你想得到的结果已经得到了,协程不需要执行了。如果返回false,本协程就会挂起。
    2)await_suspend:可以选择返回 void , bool , std::coroutine_handle<P> (P为本协程promise类型,为void时可以不写)之一。挂起awaitable。该函数会传入一个coroutine_handle类型(标识本协程)的参数handle,这是一个由编译器生成的变量。在这个函数中控制什么时候恢复协程(通过调用handle.resume())。

    i> 如果 await_suspend 返回类型是 std::coroutine_handle<Z>, 那么就会恢复这个 handle. 即运行 await_suspend(hanle).resume(). 这意味着暂停本协程的时候, 可以恢复另一个协程
    ii> 如果 await_suspend 返回类型是 bool, 那么看返回结果, 是 false 就恢复自己.
    iii> 如果 await_suspend 返回类型是 void, 那么就直接执行. 执行完暂停本协程

    3)await_resume:返回值就是co_await运算符的返回值。当协程重新运行时,会调用该函数。

    系统库已经提供了std::suspend_never, std::suspend_always可供调用。当然也可以自己实现定制的awaitable类型,例如:

    struct Action{											// 名称任意
    	bool await_ready() noexcept{return false;}			// 必须实现此接口
    	void await_suspend(coroutine_handle<>) noexcept	{}	// 必须实现此接口, 可通过此处在函数内部获取到handle
    	void await_resume() noexcept{}						// 必须实现此接口
    }
    

    (3)最简单的promise类型需要实现6个函数:

    get_return_object // to create return object
    initial_suspend // entering the coroutine body
    return_value // called when co_return called
    return_void // called before the end of coroutine body
    yield_value // called when co_yield called
    final_suspend // called when coroutine ends
    unhandled_exception // handle exception
    

    4.编译器对一个协程函数做了什么:

    例如一个简单的协程函数:

    Generator f()
    {
        co_yield 1;
    }
    

    要想编译通过,返回值类型Generator中必须包含一个名为“promise_type”的promise类型struct的定义:

    struct Generator                        // 名称任意
    {
        struct promise_type;                // 名称必须为promise_type
        using handle = std::coroutine_handle<promise_type>;
        struct promise_type
        {
            promise_type() = default();     // 非必须
            int current_value;              // 非必须, 名称任意
    
            auto get_return_object() { return Generator{ handle::from_promise(*this) }; }   // 必须实现此接口
            auto initial_suspend() { return std::suspend_always{}; }        // 必须实现此接口, 返回值必须为awaitable类型
            auto final_suspend() noexcept { return std::suspend_always{}; }      // 必须实现此接口, 返回值必须为awaitable类型。MSVC需要声明为noexcept,否则报错。
            void unhandled_exception() { std::terminate(); }    // 必须实现此接口, 用于处理协程函数内部抛出错误
            void return_void() {}                               // 如果协程函数内部无关键字co_return则必须实现此接口
            // void return_value() {}                           // 如果协程函数内部有关键字co_return则必须实现此接口
            auto yield_value(int value)                         // 如果协程函数内部有关键字co_yield则必须实现此接口, 返回值必须为awaitable类型
            {
                current_value = value;
                return std::suspend_always{};
            }
        };
    
        bool move_next() { 
            if (coro) {
                coro.resume();
                return !coro.done();    // done()函数用于返回是否处于 final_suspend 阶段
            }
            return false;
        }
        int current_value() { return coro.promise().current_value; }
        Generator(Generator const&) = delete;
        Generator(Generator&& rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
        ~Generator() {
            if (coro)
                coro.destroy();
        }
    
    private:
        Generator(handle h) : coro(h) {
        }
        handle coro;
    };
    

    我们的调用协程函数f的主函数如下:

    int main()
    {
        auto g = f();
        while (g.move_next())
            std::cout << g.current_value() << std::endl;
    }
    

    通过断点分析,执行步骤:
    (1)先执行promise_type() 产生一个promise对象
    (2)通过promise对象, 执行get_return_object(), 产生一个coroutine_name对象, 并记录handle
    (3)执行initial_suspend(), 根据返回值的await_ready()返回值 判断是否立即执行协程函数, 当返回值中await_ready()返回值为ture则立即执行协程函数, 否则调用返回值的await_suspend挂起协程、跳出到主函数。
    我们这里返回值是std::suspend_always,它的await_ready()始终返回false。
    (4)g.move_next()中的coro.resume() 将执行权传递给协程函数:首先执行awaitable类型(initial_suspend()返回的)的await_resume函数。然后进入协程函数接上次挂起的地方继续执行
    我们这里就是执行这行:co_yield 1;
    (5)执行"co_yield 1"这行时,调用yield_value(val)函数,同样的,根据返回值判断是否将执行权传递给主函数。如果返回值的await_ready返回false,则调用返回值的await_suspend挂起协程,跳出到主函数。
    我们这里返回的suspend_always会将执行权交给主函数。
    (6)主函数此时继续执行move_next中的"return !coro.done();" 这行,因为(4)的时候执行的是"coro.resume()"
    (7)coro.done()返回false,即move_next返回true。调用g.current_value获取并打印协程co_yield的值。
    (8)然后和第(4)类似,通过调用g.move_next()中的coro.resume() 将执行权传递给协程函数。
    因为协程函数已经执行完语句,所以准备返回,这里没有co_return,所以调用的是return_void()函数(如果有co_return,则调用的是return_value()函数)
    (9)然后调用final_suspend,协程进行收尾动作,在这阶段的 coroutine_handle::done 方法为 true,caller 可以通过这个方法判断协程是否结束,从而不再调用 resume 恢复协程
    根据final_suspend的返回值的await_ready判断是否立即析构promise对象,返回true则立即析构,否则不立即析构、将执行权交给主函数。
    注意如果是立即析构promise对象,则后续主函数无法通过promise获得值(强行调用可能会core)。
    我们这里返回的std::suspend_always的await_ready返回false。
    (10)主函数执行"return !coro.done();" 这行,move_next返回false。main函数退出while循环。
    (11)析构g,调用coro.destroy,结束。

    上面的执行步骤的效果等同于:

    {
        promise-type promise(promise-constructor-arguments); 
        try {
            co_await promise.initial_suspend(); // 创建之后 第一次暂停
            function-body // 函数体
        } catch ( ... ) {
            if (!initial-await-resume-called)
            throw; 
            promise.unhandled_exception(); 
        }
    
        final-suspend:
        co_await promise.final_suspend(); // 最后一次暂停
    }
    

    5.协程的储存空间

    (1)C++ 的设计是无栈协程, 所有的局部状态都储存在堆上.
    (2)储存协程的状态需要分配空间. 分配 frame 的时候会先搜索 promise_type 有没有提供 operator new, 其次是搜索全局范围.
    (3)有分配就可能会有失败. 如果写了 get_return_object_on_allocation_failure() 函数, 那就是失败后的办法, 代替 get_return_object() 来完成工作. (需要 noexcept)
    (4)协程结束以后的释放空间也会先在 promise_type 里面搜索 operator delete, 其次搜索全局范围.
    (5)协程的储存空间只有在运行完 final_suspend 之后才会析构, 或者你得显式调用 coro.destroy(). 否则协程的存储空间就永远不会释放. 如果你在 final_suspend 那里停下了, 那么就得在包装函数里面手动调用 coro.destroy(), 不然就会漏内存.
    (6)如果已经运行完毕了 final_suspend, 或者已经被 coro.destroy() 给析构了, 那么协程的储存空间已经被释放了. 再次对 coro 做任何的操作都会导致 seg fault.

    6.co_await与await_transform

    co_await cast-expression
    

    co_await 表达式只能出现在协程里. 协程就有与之相伴的 promise .

    如果这个 promise 提供了 await_transform(cast-expression) 的方法, 那么就会使用它, 变成
    co_await promise.await_transform(cast-expression)
    之后会看右边的 promise.await_transform(cast-expression) 或者 cast-expression 这个表达式有没有提供 operator co_await , 有的话就用上. 如果都没有的话就不转换类型了.

    这个 co_await 表达式的类型和 await_resume 的返回类型一样.
    如果这个 await_suspend(h) 抛出了异常, 那么协程立即 catch 异常, 恢复运行, 然后再次抛出异常.

    7.coroutine_traits

    coroutine_traits用于在编译阶段用来查找和检测满足特定coroutine_name的promise_type。
    也就是说,一个coroutine_name类型的promise_type并不是一定要定义在coroutine_name内部。

    定义的协程函数:

    R f()
    {
        co_yield 1;
    }
    

    可能的实现:

    template<class, class...>
    struct coroutine_traits {};
     
    template<class R, class... Args>
    requires requires { typename R::promise_type; }
    struct coroutine_traits<R, Args...> {
      using promise_type = typename R::promise_type;
    };
    

    编译器发现有coroutine函数时,会根据R,args查找struct std::coroutine_traits<R, args…>,如果没有,则产生一个(如上,使用R内部的promise_type定义),如果已被定义,就用已被定义的。然后检查struct std::coroutine_traits<R, args…>::promise_type是否满足如下描述的coroutine接口:

    coroutine接口要求具有public promise_type结构,而promise_type中必须有get_return_object,initial_suspend,final_suspend,unhandled_exception四个函数;
    另外,如有co_return调用,则promise_type结构中需要return_void/return_value函数;
    有co_yield调用时,需要yield_value函数;
    有co_await调用时,需要重载操作符co_await await_transform(expr)或expr,其中await_transform是promise_type结构中一个可选的函数。

    如果成功的话,就可以认为该函数是正常的协程函数了。
    实际的情况也许比较复杂,因为f()可能是静态函数,非静态函数或右值的非静态函数,对此,cppreference是这样描述的:

    (1)静态函数时,promise_type的定义为std::coroutine_traits<R, Args…>::promise_type
    如task foo(std::string x, bool flag) 对应 std::coroutine_traits<task, std::string, bool>::promise_type
    (2)非静态函数时,promise_type的定义为std::coroutine_traits<R, ClassT &, Args…>::promise_type
    如task my_class::method1(int x) const 对应 std::coroutine_traits<task, const my_class&, int>::promise_type
    (3)右值非静态函数时,promise_type的定义为std::coroutine_traits<R, ClassT&&, Args…>::promise_type
    如task my_class::method1(int x) && 对应 std::coroutine_traits<task, my_class&&, int>::promise_type

    参考:
    https://zhuanlan.zhihu.com/p/356752742
    https://zhuanlan.zhihu.com/p/239792492
    拓展阅读:https://lewissbaker.github.io/

    展开全文
  • 研究-协程的php扩展
  • C++20-协程(coroutine)

    2021-02-05 10:06:09
    A coroutine is a function that can suspend execution to be resumed later. Coroutines are stackless: they suspend execution by returning to the caller and the data that is required to res

    原文: https://en.cppreference.com/w/cpp/language/coroutines

    Coroutines

    A coroutine is a function that can suspend execution to be resumed later. 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. This allows for sequential code that executes asynchronously (e.g. to handle non-blocking I/O without explicit callbacks), and also supports algorithms on lazy-computed infinite sequences and other uses.

    A function is a coroutine if its definition does any of the following:

    coroutine是一个可以被挂起和恢复的函数. 协程是无堆栈的:它们通过返回到调用者来暂停执行,恢复执行所需的数据与堆栈分开存储。这允许异步执行的顺序代码(例如,在没有显式回调的情况下处理非阻塞I/O),也支持延迟计算无限序列的算法和其他用途。

    如果一个函数的定义有以下任何一种情况,那么它就是协程:

    • 使用co_await操作符暂停执行,直到恢复

      task<> tcp_echo_server() {
        char data[1024];
        for (;;) {
          size_t n = co_await socket.async_read_some(buffer(data));
          co_await async_write(socket, buffer(data, n));
        }
      }
      
    • 使用关键字co_yield暂停执行,返回一个值

      generator<int> iota(int n = 0) {
        while(true)
          co_yield n++;
      }
      
    • 使用关键字co_return完成执行,返回一个值

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

    每个协程都必须有一个返回类型来满足以下的许多要求。

    Restrictions
    Coroutines cannot use variadic arguments, plain return statements, or placeholder return types (auto or Concept).

    Constexpr functions, constructors, destructors, and the main function cannot be coroutines.

    限制条件:

    协程不能使用可变参数( variadic arguments)、普通返回(return)语句或占位符返回类型(auto或Concept)。Constexpr函数构造函数析构函数main函数不能是协程。

    执行

    Execution
    Each coroutine is associated with

    • the promise object, manipulated from inside the coroutine. The coroutine submits its result or exception through this object.
    • the coroutine handle, manipulated from outside the coroutine. This is a non-owning handle used to resume execution of the coroutine or to destroy the coroutine frame.
    • the coroutine state, which is an internal, heap-allocated (unless the allocation is optimized out), object that contains
      • the promise object
      • the parameters (all copied by value)
      • some representation of the current suspension point, so that resume knows where to continue and destroy knows what local variables were in scope
      • local variables and temporaries whose lifetime spans the current suspension point

    每个coroutine的关联对象:

    • promise对象,从协程内部操纵。协程通过此对象提交其结果或异常。
    • corotine handle (协程句柄),从协程外部操纵。这是一个非所有者(non-owning)句柄,用于恢复协程的执行或销毁协程帧。
    • coroutine state (协程状态),它是一个内部的堆分配对象(除非分配被优化),包含:
      • promise对象
      • 参数(都是通过值拷贝)
      • 当前挂起点的一些标记信息(representation),这样resume就知道在哪里继续,destroy就知道哪些局部变量在作用域中
      • 生存期跨越当前挂起点的局部变量和临时变量

    When a coroutine begins execution, it performs the following:

    • allocates the coroutine state object using operator new (see below)
    • copies all function parameters to the coroutine state: by-value parameters are moved or copied, by-reference parameters remain references (and so may become dangling if the coroutine is resumed after the lifetime of referred object ends)
    • calls the constructor for the promise object. If the promise type has a constructor that takes all coroutine parameters, that constructor is called, with post-copy coroutine arguments. Otherwise the default constructor is called.
    • calls promise.get_return_object() and keeps the result in a local variable. The result of that call will be returned to the caller when the coroutine first suspends. Any exceptions thrown up to and including this step propagate back to the caller, not placed in the promise.
    • calls promise.initial_suspend() and co_awaits its result. Typical Promise types either return a suspend_always, for lazily-started coroutines, or suspend_never, for eagerly-started coroutines.
    • when co_await promise.initial_suspend()resumes, starts executing the body of the coroutine

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

    • 使用 operator new 分配协程状态对象(见下文)
    • 将所有函数形参复制到协程状态:如果是按值传参则其被移动(move)或复制,如果是引用传参则保留引用(因此,如果在被引用对象的生命周期结束后恢复协程,可能会变得悬空, 因此, 程序员注意对象的生命周期)
    • 调用promise对象的构造函数。如果promise类型有一个接受所有协程参数的构造函数,则调用该构造函数,并带有复制后的协程参数。否则,将调用默认构造函数。
    • 调用promise.get_return_object()并将结果保存在一个局部变量中。当协程第一次挂起时,该调用的结果将返回给调用者。到此步骤为止抛出的任何异常(包括此步骤)都会传播回调用者,而不是放在promise中。
    • 调用promise.initial_suspend()co_await其结果。典型的Promise类型要么为lazily-started(慢启动)协程返回一个suspend_always,要么为eagerly-started(急启动)协程返回一个suspend_never
    • co_await promise.initial_suspend()恢复时,开始执行协程体

    When a coroutine reaches a suspension point

    • the return object obtained earlier is returned to the caller/resumer, after implicit conversion to the return type of the coroutine, if necessary.

    When a coroutine reaches the co_return statement, it performs the following:

    • calls promise.return_void() for
      • co_return;
      • co_return expr where expr has type void
      • falling off the end of a void-returning coroutine. The behavior is undefined if the Promise type has no Promise::return_void() member function in this case.
    • or calls promise.return_value(expr) for co_return expr where expr has non-void type
    • destroys all variables with automatic storage duration in reverse order they were created.
    • calls promise.final_suspend() and co_awaits the result.

    If the coroutine ends with an uncaught exception, it performs the following:

    • catches the exception and calls promise.unhandled_exception() from within the catch-block
    • calls promise.final_suspend() and co_awaits the result (e.g. to resume a continuation or publish a result). It’s undefined behavior to resume a coroutine from this point.

    When the coroutine state is destroyed either because it terminated via co_return or uncaught exception, or because it was destroyed via its handle, it does the following:

    • calls the destructor of the promise object.
    • calls the destructors of the function parameter copies.
    • calls operator delete to free the memory used by the coroutine state
    • transfers execution back to the caller/resumer.

    当协程到达一个暂停点时

    • 如果需要,在隐式转换为协程的返回类型之后,前面获得的返回对象返回给caller/resumer。

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

    • 调用promise.return_void()
      • co_return;
      • co_return expr 其中exprvoid类型
      • 从返回空值的协程的末尾脱落。在这种情况下,如果Promise类型没有Promise::return_void()成员函数,则该行为是未定义(undefined的。
    • 或者调用promise.return_value(expr)来获取co_return expr,其中expr为非void类型
    • 按创建时的相反顺序销毁所有自动变量。
    • 调用promise.final_suspend()co_await结果。

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

    • 捕获异常并在catch块中调用promise.unhandled_exception()
    • 调用promise.final_suspend()co_await结果(例如恢复延续或发布结果)。从这一点恢复协程是未定义的行为。

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

    • 调用promise对象的析构函数。
    • 调用函数参数副本的析构函数。
    • 调用operator delete来释放协程状态所使用的内存
    • 将执行传输回caller/resumer。

    堆分配

    Heap allocation
    coroutine state is allocated on the heap via non-array operator new.

    If the Promise type defines a class-level replacement, it will be used, otherwise global operator new will be used.

    If the Promise type defines a placement form of operator new that takes additional parameters, and they match an argument list where the first argument is the size requested (of type std::size_t) and the rest are the coroutine function arguments, those arguments will be passed to operator new (this makes it possible to use leading-allocator-convention for coroutines)

    The call to operator new can be optimized out (even if custom allocator is used) if

    • The lifetime of the coroutine state is strictly nested within the lifetime of the caller, and
    • the size of coroutine frame is known at the call site

    in that case, coroutine state is embedded in the caller’s stack frame (if the caller is an ordinary function) or coroutine state (if the caller is a coroutine)

    If allocation fails, the coroutine throws std::bad_alloc, unless the Promise type defines the member function Promise::get_return_object_on_allocation_failure(). If that member function is defined, allocation uses the nothrow form of operator new and on allocation failure, the coroutine immediately returns the object obtained from Promise::get_return_object_on_allocation_failure() to the caller.

    协程状态是通过非数组操作符new在堆上分配的。
    如果Promise类型定义了类级别的operator new,则使用它,否则将使用全局 operator new
    如果Promise类型定义了一个需要额外的参数的operator new作为替代,和他们匹配一个参数列表,第一个参数是请求的大小(类型的std:: size_t),其余是协同程序函数参数,这些参数将传递给operator new的(这使它可以使用leading-allocator-convention协程)

    对operator new的调用可以优化出来(即使使用了自定义分配器),如果:

    • 协程状态的生存期严格嵌套在调用者的生存期内,并且
    • 协程帧的大小在调用站点是已知的
      在这种情况下,协程状态被嵌入到调用者的堆栈框架中(如果调用者是一个普通函数)或协程状态(如果调用者是一个协程)

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

    Promise

    The Promise type is determined by the compiler from the return type of the coroutine using std::coroutine_traits.
    Formally, let R and Args… denote the return type and parameter type list of a coroutine respectively, ClassT and /cv-qual/ (if any) denote the class type to which the coroutine belongs and its cv-qualification respectively if it is defined as a non-static member function, its Promise type is determined by:

    • std::coroutine_traits<R, Args...>::promise_type, if the coroutine is not defined as a non-static member function,
    • std::coroutine_traits<R, ClassT /*cv-qual*/&, Args...>::promise_type, if the coroutine is defined as a non-static member function that is not rvalue-reference-qualified,
    • std::coroutine_traits<R, ClassT /*cv-qual*/&&, Args...>::promise_type, if the coroutine is defined as a non-static member function that is rvalue-reference-qualified.

    For example:

    • If the coroutine is defined as task<float> foo(std::string x, bool flag);, then its Promise type is std::coroutine_traits<task<float>, std::string, bool>::promise_type.
    • If the coroutine is defined as task<void> my_class::method1(int x) const;, its Promise type is std::coroutine_traits<task<void>, const my_class&, int>::promise_type.
    • If the coroutine is defined as task<void> my_class::method1(int x) &&;, its Promise type is std::coroutine_traits<task<void>, my_class&&, int>::promise_type.

    Promise类型由编译器根据使用std::coroutine_traits的协程返回类型确定。正式地,设RArgs…分别表示协程的返回类型和参数类型列表,classsT`和/cv-qual/(如果有的话)分别表示协程所属的类类型和它的cv限定条件。如果它被定义为一个非静态成员函数,它的Promise类型由:

    • std::coroutine_traits<R, Args...>::promise_type,如果协程未定义为非静态成员函数,
    • std::coroutine_traits<R, ClassT /*cv-qual*/&, Args...>::promise_type, 如果协程定义为非rvalue-reference限定的非静态成员函数,
    • std::coroutine_traits<R, ClassT /*cv-qual*/&&, Args...>::promise_type, 如果协程定义为rvalue-reference限定的非静态成员函数。

    举例:

    • 如果coroutine被定义为 task<float> foo(std::string x, bool flag);, 那么它的Promise类型是 std::coroutine_traits<task<float>, std::string, bool>::promise_type.
    • 如果coroutine被定义为 task<void> my_class::method1(int x) const;, 那么它的Promise类型是 std::coroutine_traits<task<void>, const my_class&, int>::promise_type.
    • 如果coroutine被定义为 task<void> my_class::method1(int x) &&;, 那么它的Promise类型是 std::coroutine_traits<task<void>, my_class&&, int>::promise_type.

    co_await

    The unary operator co_await suspends a coroutine and returns control to the caller. Its operand is an expression whose type must either define operator co_await, or be convertible to such type by means of the current coroutine’s Promise::await_transform

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

    co_await expr		
    

    First, expr is converted to an awaitable as follows:

    • if expr is produced by an initial suspend point, a final suspend point, or a yield expression, the awaitable is expr, as-is.
    • otherwise, if the current coroutine’s Promise type has the member function await_transform, then the awaitable is promise.await_transform(expr)
    • otherwise, the awaitable is expr, as-is.

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

    • 如果expr是由初始挂起点、最终挂起点或yield表达式生成的,则可等待对象按实际情况为expr。
    • 否则,如果当前协程的Promise类型有成员函数await_transform,那么可等待对象就是Promise .await_transform(expr)
    • 否则,可等待对象就是expr。

    If the expression above is a prvalue, the awaiter object is a temporary materialized from it. Otherwise, if the expression above is an glvalue, the awaiter object is the object to which it refers.

    如果上面的表达式是prvalue,则awaiter对象是它的临时实体化对象。否则,如果上面的表达式是glvalue,则awaiter对象就是它所引用的对象。

    Then, awaiter.await_ready() is called (this is a short-cut to avoid the cost of suspension if it’s known that the result is ready or can be completed synchronously). If its result, contextually-converted to bool is false then

    然后,调用await .await_ready()如果知道结果已经就绪或可以同步完成,这是一种避免挂起代价的捷径)。如果它的结果,上下文转换为bool则为false

    The coroutine is suspended (its coroutine state is populated with local variables and current suspension point).
    awaiter.await_suspend(handle) is called, where handle is the coroutine handle representing the current coroutine. Inside that function, the suspended coroutine state is observable via that handle, and it’s this function’s responsibility to schedule it to resume on some executor, or to be destroyed (returning false counts as scheduling)

    协程被挂起(它的协程状态由局部变量和当前挂起点填充)。调用await .await_suspend(句柄),其中句柄是表示当前协程的协程句柄。在这个函数内部,挂起的协程状态是可以通过这个句柄观察到的,这个函数的责任是安排它在某些执行器上恢复,或被销毁(返回错误计数作为调度)。

    • if await_suspend returns void, control is immediately returned to the caller/resumer of the current coroutine (this coroutine remains suspended), otherwise
    • if await_suspend returns bool,
      • the value true returns control to the caller/resumer of the current coroutine
      • the value false resumes the current coroutine.
    • if await_suspend returns a coroutine handle for some other coroutine, that handle is resumed (by a call to handle.resume()) (note this may chain to eventually cause the current coroutine to resume)
    • if await_suspend throws an exception, the exception is caught, the coroutine is resumed, and the exception is immediately re-thrown
    • 如果await_suspend返回void,则控制权立即返回给当前协程的调用者/恢复者(该协程保持挂起状态),否则

      • 如果await_suspend返回bool值,
      • 值true将控制权返回给当前协程的调用者/恢复者
      • 如果值为false,则恢复当前协程。
    • 如果await_suspend返回其他协程的协程句柄,该句柄将被恢复(通过调用handle.resume())(注意这可能导致当前协程最终恢复)

    • 如果await_suspend抛出异常,异常被捕获,协程被恢复,异常立即被重新抛出

    Finally, awaiter.await_resume() is called, and its result is the result of the whole co_await expr expression.
    If the coroutine was suspended in the co_await expression, and is later resumed, the resume point is immediately before the call to awaiter.await_resume().

    最后,调用await .await_resume(),其结果是整个co_await expr表达式的结果。
    如果协程在co_await表达式中被挂起,然后被恢复,恢复点就在调用await .await_resume()之前。

    Note that because the coroutine is fully suspended before entering awaiter.await_suspend(), that function is free to transfer the coroutine handle across threads, with no additional synchronization. For example, it can put it inside a callback, scheduled to run on a threadpool when async I/O operation completes. In that case, since the current coroutine may have been resumed and thus executed the awaiter object’s destructor, all concurrently as await_suspend() continues its execution on the current thread, await_suspend() should treat *this as destroyed and not access it after the handle was published to other threads.

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

    例子

    #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);
    }
    

    需要使用gcc 10.2进行编译

    $ g++ --version
    g++ (Ubuntu 10.2.0-5ubuntu1~20.04) 10.2.0
    $ g++ coroutine.cc -std=c++20 -fcoroutines
    $ ./a.out 
    Coroutine started on thread: 140421255046976
    New thread ID: 140421255042816
    Coroutine resumed on thread: 140421255042816
    

    This section is incomplete
    Reason: examples

    这一部分尚未完成

    co_yield

    co_yield
    Yield-expression returns a value to the caller and suspends the current coroutine: it is the common building block of resumable generator functions
    co_yield expr
    co_yield braced-init-list
    It is equivalent to
    co_await promise.yield_value(expr)
    A typical generator’s yield_value would store (copy/move or just store the address of, since the argument’s lifetime crosses the suspension point inside the co_await) its argument into the generator object and return std::suspend_always, transferring control to the caller/resumer.

    Yield-expression返回一个值给调用者,并挂起当前协程:它可以构建可恢复生成器函数(类似python中的yield)

    co_yield expr		
    co_yield braced-init-list	
    

    它等价于

     co_await promise.yield_value(expr)
    

    一个典型的生成器的yield_value将其参数存储(复制/移动或仅仅存储其地址,因为参数的生命周期跨越了co_await内部的悬挂点)到生成器对象中,并返回std:: susend_always,将控制权转移给caller/resumer。

    This section is incomplete
    Reason: examples
    这一部分尚未完成

    展开全文
  • } } explicit resumable_thing(coroutine_handle coroutine) : _coroutine(coroutine) { } ~resumable_thing() { if (_coroutine) { _coroutine.destroy(); } } void resume() { _coroutine.resume(); } }; ...

    72fadd92a96cfc541fc89376cbc1c15c.png

    导语 | 本篇文章循序渐进地介绍C++20协程的方方面面,先从语言机制说起,再来介绍如何基于C++20的设施实现一个对比C++17来说更简单易用,约束性更好的一个任务调度器,最后结合一个简单的实例来讲述如何在开发中使用这些基础设施。

    Vue框架通过数据双向绑定和虚拟DOM技术,帮我们处理了前端开发中最脏最累的DOM操作部分,我们不再需要去考虑如何操作DOM以及如何最高效地操作DOM,但是我们仍然需要去关注Vue在跨平台项目性能方面的优化,使项目具有更高效的性能、更好的用户体验。

    一、C++20 Coroutine机制简介

    要理解C++20的coroutine,首先我们需要了解coroutine这个概念。协程本身不是什么新鲜概念,在打孔计算机时代就已经存在。一个coroutine与一个function十分相似,都可以被调用,并且可以返回具体的结果。区别在于,普通函数只能一次调用一次返回,而coroutine是可以多次调用并且多次返回的,并且coroutine本身具有状态,多次返回的值可以不一样。

    我们主要也是利用协程的这个特性,利用协程可以挂起(yield)->执行(resume)->挂起->执行的这个特点,来组织我们的任务调度器。这个特性也是我们通常所说的软中断(soft trap),软中断是操作系统进行各类调用的基础,我们使用协程特性来实现业务层的任务调度器,本身也是一个很自然的事情。

    通过软中断去理解协程,就比较直观了,中断肯定就意味着有执行上下文的保存和恢复,所以整个协程的执行过程,其实就是多次的上下文保存跳出(yield),上下文恢复重入(resume),直至协程最终执行完成。而yield时候的返回值,和resume时的传入值,就变成了我们与一个既定协程交换数据的手段了。

    注意coroutine这种多次调用返回不同值的特性也常被用于generator或者iterator,不过我们本篇的重点是如何基于coroutine来实现任务调度器,这部分暂且搁置,感兴趣的朋友可以自行查阅资料尝试,这部分比较简单,这里不再详细叙述了。

    (一)C++20 coroutine理解的难点

    C++20 coroutine大致的概览图如下:

    0a0c1a49757ec9abf8d0d737cc8f03f1.png

    从上图可以看出C++ coroutine20实现的几个特点:

    1. 依赖coroutine_handle<>对象管理协程本身的生命周期。

    2. 依赖promise_type对象对协程的一些行为(如启动挂起,执行结束前挂起等)进行配置, 传递返回值。

    3. co_await机制配合Awaitable对象完全协程的挂起点定义以及协程与外界的数据交换。

    对比Lua等语言的协程实现,C++20的协程实现会显得比较别扭,作为一个无VM的强类型语言,C++20是通过Compiler代码生成与语法糖配合的模式来实现的相关机制,而前者其实是直接在VM级别提供的相关设施,对比之下,C++20协程使用的直观度,便利性都会存在一些折扣,但我们通过一定的包装,会发现其实C++20协程提供的特性已经很强大了,我们可以实现业务侧使用负担尽可能低的任务调度器。


    (二)结合代码片段理解C++协程

    网上讲C++协程关键字和相关类型的文章比较多,此处不做搬运了,在文章参考处也会给出一些笔者觉得写得比较好的文章的链接供大家参考。我们主要通过一个代码片断来理解任务管理器部分重点使用的co_awiat的运行机制和co_return的运行机制。

    #include <iostream>
    #include <resumable>
    
    
    using namespace std;
    
    
    struct resumable_thing
    {
      struct promise_type
      {
        resumable_thing get_return_object()
        {
          return resumable_thing(coroutine_handle<promise_type>::from_promise(*this));
        }
        auto initial_suspend() { return suspend_never{}; }
        auto final_suspend() { return suspend_never{}; }
        void return_void() {}
      };
      coroutine_handle<promise_type> _coroutine = nullptr;
      resumable_thing() = default;
      resumable_thing(resumable_thing const&) = delete;
      resumable_thing& operator=(resumable_thing const&) = delete;
      resumable_thing(resumable_thing&& other)
        : _coroutine(other._coroutine) {
          other._coroutine = nullptr;
        }
      resumable_thing& operator = (resumable_thing&& other) {
        if (&other != this) {
          _coroutine = other._coroutine;
          other._coroutine = nullptr;
        }
      }
      explicit resumable_thing(coroutine_handle<promise_type> coroutine) : _coroutine(coroutine)
      {
      }
      ~resumable_thing()
      {
        if (_coroutine) { _coroutine.destroy(); }
      }
      void resume() { _coroutine.resume(); }
    };
    
    
    resumable_thing counter() {
      cout << "counter: called\n";
      for (unsigned i = 1; ; i++)
      {
        co_await std::suspend_always{};
        cout << "counter:: resumed (#" << i << ")\n";
      }
    }
    
    
    int main()
    {
      cout << "main:    calling counter\n";
      resumable_thing the_counter = counter();
      cout << "main:    resuming counter\n";
      the_counter.resume();
      the_counter.resume();
      the_counter.resume();
      the_counter.resume();
      the_counter.resume();
      cout << "main:    done\n";
      return 0;
    }
      上面代码的输出如下:
    
    
    main:    calling counter
    counter: called
    main:    resuming counter
    counter:: resumed (#1)
    counter:: resumed (#2)
    counter:: resumed (#3)
    counter:: resumed (#4)
    counter:: resumed (#5)
    main:    done
    • 由Compiler展开的代码


    上面的示例代码中,我们注意到counter()是一个协程函数,可以多次被挂起和重入。我们也来看一下Compiler对协程函数进行加工后,大概的counter的定义。

    注意:此处Compiler还有进一步的代码生成过程和调整,我们只关注到当前这一层。

    我们要在C++20中定义一个coroutine,对函数的返回值是有要求的,这个返回值的类型必须有一个嵌套的子类型promise_type。因为我们更多的是利用coroutine特性来完成一个调度器的包装,先忽略用不上特殊的函数模板coroutine_traits<>这种特性。

    resumable_thing counter() {
      __counter_context* __context = new __counter_context{};
      __return = __context->_promise.get_return_object();
      co_await __context->_promise.initial_suspend();
        
      cout << "counter: called\n";
      for (unsigned i = 1; ; i++)
      {
        co_await std::suspend_always{};
        cout << "counter:: resumed (#" << i << ")\n";
      }
    __final_suspend_label:
      co_await __context->_promise.final_suspend();
    }
    • 理解promise_type

    struct resumable_thing
    {
      struct promise_type
      {
        resumable_thing get_return_object();
        auto initial_suspend() { return suspend_never{}; }
        auto final_suspend() { return suspend_never{}; }
        void return_void() {}
      };

    我们注意到上一节生成的代码里,promise_type中定义的get_return_object(), initial_suspend(),final_suspend()等函数都被插入到编译器调整之后的counter()代码中了。我们先从最开始的counter_context看起。__counter_context是编译器生成的一个上下文,用于保存coroutine挂起还原时所需要的动态空间(如果不需要这个空间,编译器会把这个分配操作优化掉)。counter()的返回值类型是resumable_thing,但实际上,该返回类型是通过以下代码:

    __return = __context->promise.get_return_object();

    来创建的__return对象。

    在正式执行coroutine之前,会先执行前面定义的promise对象的initial_suspend(),以些来判断是否需要一开始就需要挂起协程,上面的示例代码返回的suspend_never{},相关的代码:

    co_await __context->_promise.initial_suspend();

    不会执行挂起的操作,会继续往下执行。相反,如果我们返回suspend_always{},那么counter() 则会马上挂起。

    同样,在coroutine执行结束之前,则会执行前面定义的promise对象的final_suspend(),看是否需要结束前挂起,机制与执行前的挂起一致。

    同样,对于co_return来说,会被compiler改写为下列代码,其实就是一个简单的语法糖,先调用promise对象的return_void()或者return_value(),最后直接goto到协程结束处:

    __context->_promise->return_void(); goto final_suspend_label;

    对于co_yield,也有类似co_return的代码替换处理过程。比如 co_yield“hello”,会被compiler改写为下面的代码,可以看到co_yield其实只是co_await的语法糖,会先调用promise对象的yield_value()方法,所以如果对应的promise对象没有实现相应类型的yield_value()重载,编译器会直接报错:

    co_await __context->_promise->yield_value("hello");

    C++框架的实现主要是利用co_await来实现挂起语义,并没有使用co_yield,避免业务向的相关实现大量集中到promise处,导致过强的代码依赖。

    通过上面的描述,不难发现,C++20的协程机制中,promise对象起到的作用是“从内部控制协程,将异常和结果传递给外部系统”这样的一个桥梁作用。

    • 理解coroutine_handle

    上一节说到Promise对象的作用是“从内部控制协程,将异常和结果传递给业务系统”,那么相对应的,coroutine_handle<>其实就是外部系统对协程生命周期进行控制的对象,我们也很容易看到在前面我们定义的resumable_thing类中,关于coroutine_handle<>的使用, 我们可以利用coroutine_handle<>对象来对协程进行resume()和destroy()等操作。

    struct resumable_thing
    {
      coroutine_handle<promise_type> _coroutine = nullptr;
      ~resumable_thing()
      {
        if (_coroutine) { _coroutine.destroy(); }
      }
      void resume() { _coroutine.resume(); }
    };

    coroutine_handle也是一个模板,coroutine_handle定义如下:

    template <> struct coroutine_handle<void>{
      constexpr coroutine_handle() noexcept;
      constexpr coroutine_handle(nullptr_t) noexcept;
      coroutine_handle& operator=(nullptr_t) noexcept;
      constexpr void* address() const noexcept;
      constexpr static coroutine_handle from_address(void* addr);
      constexpr explicit operator bool() const noexcept;
      bool done() const;
      void operator()();
      void resume();
      void destroy();
    private:
      void* ptr;// exposition only
    };

    通常针对每种promise_type,会从coroutine_handle<>派生出相应的针对此种promise_type的特化版的coroutine_handle,如上例中使用的coroutine_handle< resumable_thing::promise_type >:

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

    coroutine_handle用于控制coroutine的生命周期。比如,coroutine_handle的resume()用来恢复coroutine的执行;destroy()用来释放用于保存coroutine状态而分配额动态内存;done()用于告知一个coroutine是否已经destoy;operator()()用于coroutine的初次执行。

    有两个条件能让coroutine释放,一个是显示调用destroy();另一个是coroutine执行完final_suspend之后自动释放。这里需要注意的是,不能让coroutine释放两次,否则跟free内存两次额效果类似。

    现在让我们回到promise_type的get_return_object(),可以看到它传了一个coroutine_handle给resumable_thing的构造函数。随后resumable_thing可以通过这个传入的coroutine_handle来控制coroutine的执行,示例代码中也是通过这种方式来实现的协程的resume():

    resumable_thing get_return_object()
    {
      return resumable_thing(coroutine_handle<promise_type>::from_promise(*this));
    }
    • co_await与awaitable对象

    前面我们也简单介绍了C++20的协程通过co_await与awaitable对象来完成协程执行的挂起,以及协程与外界的数据交换。具体是怎么做到的呢?我们可以先来看看编译器遇到:

    co_await awaitable;

    时的处理, 这个操作会被编译器改写成:

    if (not awaitable.await_ready()) {
      // suspend point;
      awaitable.await_suspend(coroutine_handle);
      // return to the caller
      // resume point;
    }
    awaitable.await_resume();

    对于一个awaitable的定义来说,主要由三个函数组成:

    1. await_ready(): 用于判断是否需要挂起,如果返回false,则协程会继续执行,不会执行挂起操作。

    2. await_suspend(): 协程挂起后需要执行的操作可以添加在此处,一般此处填充的是一个异步操作,以及对当前协程状态的记录和设置的操作。

    3. await_resume(): 协程恢复执行的时候调用的操作。同时如果对应的异步操作需要外部返回值到协程处,我们会在此处操作。

    综合来看, 通过这样的设置,C++20 coroutine给使用者提供了一个对挂起和恢复进行定制的机制,有了这样的机制,我们就可以通过实现不同的awaitable来实现各种异步操作的协程化,这也是下文中的Scheduler实际使用的方法。

    除了上面介绍的awaitable对象外,C++20还提供了其他形式的awaitable,按编译器默认处理的优先次序列出如下:

    1. promise_type::await_transform(): 在promise_type上重载不同类型参数的await_transform()。

    2. operator co_await(): 为co_await调用的对象重载operator co_await()操作符。

    3. awaitable对象:上面重点介绍的实现了await_ready(),await_suspend(),await_resume()的awaitable对象,也是我们最终实现调度器采用的方式。

    • 小结

    至此我们已经基本介绍完了C++20 coroutine相关的特性了。coroutine机制出现的时间其实还挺早的,其他语言中也都存在,比如:

    Dart1.9:

    Future<int> getPage(t) async {
      var c = new http.Client();
      try {
        var r = await c.get('http://url/search?q=$t');
        print(r);
        return r.length();
      } finally {
        await c.close();
      }
    }

    Python:

    async def abinary(n):
      if n <= 0:
      return 1
      l = await abinary(n - 1)
      r = await abinary(n - 1)
      return l + 1 + r

    C#:

    async Task<string> WaitAsynchronouslyAsync()
    {
      await Task.Delay(10000);
      return "Finished";
    }

    从某种角度看,coroutine可以看成是一个通用化的函数(Generalized Function),区别于传统的单入口单出口的函数(Plain Old Function)之外,增加若干可能性:

    • Monadic*(await-suspend)

    • Task(await)

    • Generator(yield)

    • Async Generator(await+yield)

    二、C++20版的协程调度器 

    (一)Scheduler实现的动机

    有了上面介绍的功能强大的C++20的coroutine,我们可以更好的实现我们的调度器。


    (二)Scheduler核心机制

    543de24071ed83dbfed27b43ba0efaa7.png

    如上图所示,Scheduler主要提供对SchedTask的管理,以及两个基础机制方便协程相关业务机制的实现:

    1. Awaitable机制: 前面也介绍了利用C++20的co_await关键字和awaitable对象,我们可以很好的定义挂起点,以及交换协程和外部系统的数据。

    2. Return Callback机制: 部分协程执行完后需要向外界反馈执行结果(如协程模式执行的Rpc Service)。

    (三)Scheduler核心对象


    •  ISchedTask & SchedTaskCpp20

    using CoReturnFunction = std::function<void(const CoReturnObject*)>;
    
    
    class ISchedTask
    {
        friend class Scheduler;
      public:
        ISchedTask() = delete;
        ISchedTask(const SchedTaskCpp17&) = delete;
        ISchedTask(uint64_t taskId, Scheduler* manager);
        virtual ~ISchedTask();
        uint64_t GetId() const;
        virtual int Run() = 0;
        virtual bool IsDone() const = 0;
        virtual CO_TASK_STATE GetCoState() const = 0;
        void BindSleepHandle(uint64_t handle);
        AwaitMode GetAwaitMode() const;
        int GetAwaitTimeout() const;
        template<typename AwaitEventType>
        auto BindResumeObject(AwaitEventType&& awaitEvent)->std::enable_if_t<std::is_base_of<ResumeObject, AwaitEventType>::value>;
        template<typename AwaitEventType>
        auto GetResumeObjectAsType()->std::enable_if_t<std::is_base_of<ResumeObject, AwaitEventType>::value, AwaitEventType*>;
        bool HasResumeObject() const noexcept;
        void ClearResumeObject();
        bool IsLastInvokeSuc() const noexcept;
        bool IsLastInvokeTimeOut() const noexcept;
        bool IsLastInvokeFailed() const noexcept;
        void AddChildTask(uint64_t tid);
        void AddWaitNofityTask(uint64_t tid);
        const auto& GetChildTaskArray() const;
        const auto& GetWaitNotifyArray() const;
        void Terminate();
        Scheduler* GetManager() const;
        static ISchedTask* CurrentTask();
        void DoYield(AwaitMode mode, int awaitTimeMs = 0);
        void SetReturnFunction(CoReturnFunction&& func);
        void DoReturn(const CoReturnObject& obj);
        void DoReturn();
      protected:
        uint64_t          mTaskId;
        Scheduler*           mManager;
        std::vector<uint64_t>    mChildArray;
        std::vector<uint64_t>    mWaitNotifyArray;
        //value used to return from coroutine
        AwaitMode          mAwaitMode = AwaitMode::AwaitDoNothing;
        int              mAwaitTimeout = 0;
        //value used to send to coroutine(now as a AwaitEvent)
        reflection::UserObject    mResumeObject;
        uint64_t          mSleepHandle = 0;
        bool            mIsTerminate = false;
        CoReturnFunction      mCoReturnFunc;
    };
    
    
    class SchedTaskCpp20: public ISchedTask
    {
      public:
        SchedTaskCpp20(uint64_t taskId, CoTaskFunction&& taskFunc, Scheduler* manager);
        ~SchedTaskCpp20();
        int Run() override;
        bool IsDone() const override;
        CO_TASK_STATE GetCoState() const override;
        void BindSelfToCoTask();
        const CoResumingTaskCpp20& GetResumingTask() const;
      protected:
        CoResumingTaskCpp20      mCoResumingTask;
        CoTaskFunction        mTaskFuncion;
    };

    C++20的SchedTaskCpp20主要完成对协程对象的封装,CoTaskFunction用于存储相关的函数对象,而CoResumingTaskCpp20则如同前面示例中的resumable_thing对象,内部有需要的promise_type实现,我们对协程的访问也是通过它来完成的。

    此处需要注意的是我们保存了协程对象外,还额外保存了相关的函数对象,这是因为如果协程本身是一个lambda,compiler并不会帮我们正确维护lambda的生命周期以及lambda所捕获的函数,尚未清楚是实现缺陷还是功能就是如此,所以此处需要一个额外存在的std::function<>对象,来保证对应lambda的生命周期是正确的。

    我们的SchedTask对象中主要保留了:reflection::UserObject mResumeObject: 主要用于异步等待的执行,当一个异步等待成功执行的时候,向协程传递值。

    原来利用事件去处理最终返回值的机制也替换成了Return回调的方式,相对来说更简单直接,利用lambda本身也能很方便的保存需要最终回传的临时值了。

    • Scheduler


    Scheduler的代码比较多,主要就是SchedTask的管理器,另外也完成对前面提到的三种机制的支持,文章重点分析一下三种机制的实现代码。


    • Yield处理

    void Scheduler::Update()
    {
        RSTUDIO_PROFILER_METHOD_INFO(sUpdate, "Scheduler::Update()", rstudio::ProfilerGroupType::kLogicJob);
        RSTUDIO_PROFILER_AUTO_SCOPE(sUpdate);
    
    
        //Handle need kill task first
        while(!mNeedKillArray.empty())
        {
            auto tid = mNeedKillArray.front();
            mNeedKillArray.pop();
            auto* tmpTask = GetTaskById(tid);
            if (tmpTask != nullptr)
            {
                DestroyTask(tmpTask);
            }
        }
    
    
        //Keep a temp queue for not excute next frame task right now
        decltype(mFrameStartTasks) tmpFrameTasks;
        mFrameStartTasks.swap(tmpFrameTasks);
    
    
        while (!tmpFrameTasks.empty())
        {
            auto task_id = tmpFrameTasks.front();
            tmpFrameTasks.pop();
            auto* task = GetTaskById(task_id);
            LOG_CHECK_ERROR(task);
            if (task)
            {
                AddToImmRun(task);
            }
        }
    }
    
    
    void Scheduler::AddToImmRun(ISchedTask* schedTask)
    {
        LOG_PROCESS_ERROR(schedTask);
        schedTask->Run();
    
    
        if (schedTask->IsDone())
        {
            DestroyTask(schedTask);
            return;
        }
    
    
        {
            auto awaitMode = schedTask->GetAwaitMode();
            auto awaitTimeoutMs = schedTask->GetAwaitTimeout();
            switch (schedTask->GetAwaitMode())
            {
                case rstudio::logic::AwaitMode::AwaitNever:
                    AddToImmRun(schedTask);
                    break;
                case rstudio::logic::AwaitMode::AwaitNextframe:
                    AddToNextFrameRun(schedTask);
                    break;
                case rstudio::logic::AwaitMode::AwaitForNotifyNoTimeout:
                case rstudio::logic::AwaitMode::AwaitForNotifyWithTimeout:
                    {
                        HandleTaskAwaitForNotify(schedTask, awaitMode, awaitTimeoutMs);
                    }
                    break;
                case rstudio::logic::AwaitMode::AwaitDoNothing:
                    break;
                default:
                    RSTUDIO_ERROR(CanNotRunToHereError());
                    break;
            }
        }
        Exit0:
        return;
    }

    上面是Scheduler的Update()以及Update用到的核心函数AddToImmRun()的实现代码,在每个task->Run()后,到达下一个挂起点,返回外部代码的时候,外部代码会根据Task当前的AwaitMode对协程后续行为进行控制,主要是以下几种模式:

    1. rstudio::logic::AwaitMode::AwaitNever:立即将协程加入回mReadyTask队列,对应协程会被马上唤醒执行。

    2. rstudio::logic::AwaitMode::AwaitNextframe:将协程加入到下一帧执行的队列,协程将会在下一帧被唤醒执行。

    3. rstudio::logic::AwaitMode::AwaitForNotifyNoTimeout:等待外界通知后再唤醒执行(无超时模式),注意该模式下如果一直没收到通知,相关协程会一直在队列中存在。

    4. rstudio::logic::AwaitMode::AwaitForNotifyWithTimeout:同3,差别是存在一个超时时间,超时时间到了也会唤醒协程,业务方可以通过ResumeObject判断协程是被超时唤醒的。

    5. rstudio::logic::AwaitMode::AwaitDoNothing:特殊的AwaitHandle实现会使用该模式,比如删除Task的实现,都要删除Task了,我们肯定不需要再将Task加入任何可唤醒队列了。


    • Resume处理

    Resume机制主要是通过唤醒在Await队列中的协程的时候向关联的Task对象传递ResumeObject实现的:

    //Not a real event notify here, just do need things
    template <typename E>
    auto ResumeTaskByAwaitObject(E&& awaitObj) -> std::enable_if_t<std::is_base_of<ResumeObject, E>::value>
    {
        auto tid = awaitObj.taskId;
        if (IsTaskInAwaitSet(tid))
        {
            //Only in await set task can be resume
            auto* task = GetTaskById(tid);
            if (RSTUDIO_LIKELY(task != nullptr))
            {
                task->BindResumeObject(std::forward<E>(awaitObj));
                AddToImmRun(task);
            }
    
    
            OnTaskAwaitNotifyFinish(tid);
        }
    }

    然后再通过rco_get_resume_object()宏在协程代码中获取对应的ResumeObject.宏的声明代码如下:

    #define rco_get_resume_object(ResumeObjectType)

    本身就是一个简单的传值取值的过程。注意传递ResumeObject后,我们也会马上将协程加入到mReadTasks队列中以方便在接下来的Update中唤醒它。

    • 一个Awaitable实现的范例

    我们以Rpc的协程化Caller实现为例,看看一个awaitable对象应该如何构造:

    class RSTUDIO_APP_SERVICE_API RpcRequest
    {
      public:
        RpcRequest() = delete;
        RpcRequest(const RpcRequest&) = delete;
        ~RpcRequest() = default;
    
    
        RpcRequest(const logic::GameServiceCallerPtr& proxy, const std::string_view funcName, reflection::Args&& arg, int timeoutMs) :
        mProxy(proxy)
            , mFuncName(funcName)
            , mArgs(std::forward<reflection::Args>(arg))
            , mTimeoutMs(timeoutMs)
        {}
        bool await_ready()
    {
        return false;
      }
        void await_suspend(coroutine_handle<>) const noexcept
    {
            auto* task = rco_self_task();
            auto context = std::make_shared<ServiceContext>();
            context->TaskId = task->GetId();
            context->Timeout = mTimeoutMs;
            auto args = mArgs;
            mProxy->DoDynamicCall(mFuncName, std::move(args), context);
            task->DoYield(AwaitMode::AwaitForNotifyNoTimeout);
        }
        ::rstudio::logic::RpcResumeObject* await_resume() const noexcept
    {
            return rco_get_resume_object(logic::RpcResumeObject);
        }
      private:
        logic::GameServiceCallerPtr          mProxy;
        std::string                  mFuncName;
        reflection::Args              mArgs;
        int                      mTimeoutMs;
    };

    重点是前面说到的await_ready(),await_suspend(),await_resume()函数的实现。

    • ReturnCallback机制

    有一些特殊的场合,可能需要协程执行完成后向业务系统发起通知并传递返回值,比如Rpc Service的协程支持实现,这里直接以RpcService的协程支持为例来说明好了。

    首先是业务侧,在创建完协程后,需要给协程绑定后续协程执行完成后做进一步操作需要的数据:

    task->SetReturnFunction([this, server, entity, cmdHead, routerAddr, reqHead, context](const CoReturnObject* obj) {
        const auto* returnObj = dynamic_cast<const CoRpcReturnObject*>(obj);
        if (RSTUDIO_LIKELY(returnObj))
        {
            DoRpcResponse(server, entity.get(), routerAddr, &cmdHead,
                          reqHead, const_cast<ServiceContext&>(context),
                          returnObj->rpcResultType, returnObj->totalRet, returnObj->retValue);
        }
    });

    这里将Connection id等信息通过lambda的capture功能直接绑定到SchedTask的返回函数,然后业务代码会利用co_return本身的功能向promise_type传递返回值:

    CoTaskInfo HeartBeatService::DoHeartBeat(logic::Scheduler& scheduler, int testVal)
    {
        return scheduler.CreateTask20(
            [testVal]() -> logic::CoResumingTaskCpp20 {
    
    
                co_await logic::cotasks::Sleep(1000);
    
    
                printf("service yield call finish!\n");
    
    
                co_return CoRpcReturnObject(reflection::Value(testVal + 1));
            }
        );
    }

    最终我们利用promise_type的return_value()来完成对设置的回调的调用:

    void CoResumingTaskCpp20::promise_type::return_value(const CoReturnObject& obj)
    {
        auto* task = rco_self_task();
        task->DoReturn(obj);
    }

    注意这个地方task上存储的ExtraFinishObject会作为event的一部分直接传递给业务系统,并在发起事件后调用删除协程任务的方法。

    通过Return Callback的方式来对一些特殊的返回进行处理,这种机制是容易使用的。

    (四)示例代码

    //C++ 20 coroutine
    auto clientProxy = mRpcClient->CreateServiceProxy("mmo.HeartBeat");
    mScheduler.CreateTask20([clientProxy]() -> rstudio::logic::CoResumingTaskCpp20 {
    
    
        auto* task = rco_self_task();
    
    
        printf("step1: task is %llu\n", task->GetId());
    
    
    
    
        co_await rstudio::logic::cotasks::NextFrame{};
    
    
        printf("step2 after yield!\n");
        int c = 0;
        while (c < 5)
        {
            printf("in while loop c=%d\n", c);
            co_await rstudio::logic::cotasks::Sleep(1000);
            c++;
        }
    
    
        for (c = 0; c < 5; c++)
        {
            printf("in for loop c=%d\n", c);
            co_await rstudio::logic::cotasks::NextFrame{};
        }
    
    
        rco_kill_self();
    
    
        LOG_INFO(0, "step3 %d", c);
        printf("step3 %d\n", c);
    
    
        auto newTaskId = co_await rstudio::logic::cotasks::CreateTask(false, []()-> logic::CoResumingTaskCpp20 {
            printf("from child coroutine!\n");
            co_await rstudio::logic::cotasks::Sleep(2000);
            printf("after child coroutine sleep\n");
        });
    
    
        printf("new task create in coroutine: %llu\n", newTaskId);
    
    
        printf("Begin wait for task!\n");
    
    
        co_await rstudio::logic::cotasks::WaitTaskFinish{ newTaskId, 10000 };
    
    
        printf("After wait for task!\n");
    
    
        rstudio::logic::cotasks::RpcRequest rpcReq{clientProxy, "DoHeartBeat", rstudio::reflection::Args{ 3 }, 5000};
        auto* rpcret = co_await rpcReq;
        if (rpcret->rpcResultType == rstudio::network::RpcResponseResultType::RequestSuc)
        {
            assert(rpcret->totalRet == 1);
            auto retval = rpcret->retValue.to<int>();
            assert(retval == 4);
            printf("rpc coroutine run suc, val = %d!\n", retval);
        }
        else
        {
            printf("rpc coroutine run failed! result = %d \n", (int)rpcret->rpcResultType);
        }
    
    
        co_await rstudio::logic::cotasks::Sleep(5000);
    
    
        printf("step4, after 5s sleep\n");
    
    
        co_return rstudio::logic::CoNil;
    } );

    执行结果:

    step1: task is 1
    step2 after yield!
    in while loop c=0
    in while loop c=1
    in while loop c=2
    in while loop c=3
    in while loop c=4
    in for loop c=0
    in for loop c=1
    in for loop c=2
    in for loop c=3
    in for loop c=4
    step3 5
    new task create in coroutine: 2
    Begin wait for task!
    from child coroutine!
    after child coroutine sleep
    After wait for task!
    service yield call finish!
    rpc coroutine run suc, val = 4!
    step4, after 5s sleep

    对比原先版本,主要的好处是:

    1. 代码更精简了。

    2. Stack变量可以被Compiler自动处理,正常使用了。

    3. co_await可以直接返回值,并有强制的类型约束了。

    4. 一个协程函数就是一个返回值为logic::CoResumingTaskCpp20类型的lambda,可以充分利用lambda本身的特性还实现正确的逻辑了。


    三、业务向实例 

    (一)一个Python实现的技能示例

    我们以一个原来在python中利用包装的协程调度器实现的技能系统为例,先来看看相关的实现效果和核心代码。

    • 实现效果

    以下是相关实现的示例效果,主要是一个火球技能和实现和一个闪电链技能的实现:

    221449c2e406d9446c481fe1a2f46368.gif


    • 技能主流程代码

    我们先来看一下技能的主流程代码,可以发现使用协程方式实现,整个代码更函数式,区别于面向对象构造不同对象存储中间态数据的设计。

    # handle one skill instance create
    def skill_instance_run_func(instance, user, skill_data, target, target_pos, finish_func):
        # set return callback here
      yield TaskSetExitCallback(finish_func)
     
        # some code ignore here
        # ...
    
    
      from common.gametime import GameTime
      init_time = GameTime.now_time
      for skill_step in step_list:
        step_start_time = GameTime.now_time
    
    
            # some code ignore here
            # ...
    
    
            ### 1. period task handle
        if skill_step.cast_type == CastSkillStep.CAST_TYPE_PERIOD:
          if len(skill_step.cast_action_group_list) > 0:
            action_group = skill_step.cast_action_group_list[0]
            for i in range(skill_step.cast_count):
                        # yield for sleep
              yield TaskSleep(skill_step.cast_period)
              ret_val = do_skill_spend(skill_data, user, instance)
              if not ret_val:
                return
              do_one_skill_cast(skill_data, skill_step, action_group, user, instance, target_id, target_pos)
            
            ### 2. missle skill
        elif skill_step.cast_type == CastSkillStep.CAST_TYPE_MISSLE_TO_TARGET:
          if len(skill_step.cast_action_group_list) > 0:
            action_group = skill_step.cast_action_group_list[0]
            for i in range(skill_step.cast_count):
                        # yield for sleep
              yield TaskSleep(skill_step.cast_period)
              ret_val = do_skill_spend(skill_data, user, instance)
              if not ret_val:
                return
                        # sub coroutine(missle_handle_func)
              task_id = yield TaskNew(missle_handle_func(skill_data, instance, user, skill_step, action_group, target_id, target_pos))
              instance.add_child_task_id(task_id)
    
    
            ### 3. guide skill
        elif skill_step.cast_type == CastSkillStep.CAST_TYPE_GUIDE_TO_TARGET:
          if len(skill_step.cast_action_group_list) > 0:    
                    # some code ignore here
                    # ...
    
    
            for i in range(skill_step.cast_count):
                        # yield sleep
              yield TaskSleep(skill_step.cast_period)
              ret_val = do_skill_spend(skill_data, user, instance)
              if not ret_val:
                return
              do_one_skill_cast(skill_data, skill_step, action_group, user, instance, first_target.obj_id, first_target.get_position())
              start_pos = skill_step.guide_info.get_target_position(user, start_id, start_pos)[0]
              end_pos = skill_step.guide_info.get_target_position(user, first_target.obj_id, first_target.get_position())[0]
              end_id = first_target.obj_id
                        # sub coroutine(guide_handle_func)
              task_id = yield TaskNew(guide_handle_func(skill_data, instance, user, skill_step, start_pos, end_id, end_pos))
              start_pos = end_pos
              start_id = end_id
              instance.add_child_task_id(task_id)
    
    
              first_target = None
              if guide_target_list:
                pop_index = random.randrange(0, len(guide_target_list))
                first_target = guide_target_list.pop(pop_index)
              if not first_target:
                break
    
    
        now_time = GameTime.now_time
        step_pass_time = now_time - step_start_time
        need_sleep_time = skill_step.step_total_time - step_pass_time
        if need_sleep_time > 0:
          yield TaskSleep(need_sleep_time)
    
    
        instance.on_one_step_finish(skill_step)
    
    
      if skill_data.delay_end_time > 0:
        yield TaskSleep(skill_data.delay_end_time)
    
    
        # wait for child finish~~
      for task_id in instance.child_task_list:
        yield TaskWait(task_id)
    
    
      instance.task_id = 0

    整体实现比较简单,整个技能是由多个SkillStep来配置的,整体技能的流程就是for循环执行所有SkillStep,然后提供了多种SkillStep类型的处理,主要是以下几类:

    1. CastSkillStep.CAST_TYPE_PERIOD:周期性触发的技能,主要使用yield TaskSleep()

    2. CastSkillStep.CAST_TYPE_MISSLE_TO_TARGET:导弹类技能,使用子协程功能。

    3. CastSkillStep.CAST_TYPE_GUIDE_TO_TARGET:引导类技能,使用子协程功能

    最后所有step应用完毕会进入配置的休眠和等待子任务的阶段。

    • 子任务-导弹类技能相关代码

    对于上面介绍的导弹类技能(火球),核心实现也比较简单,实现了一个飞行物按固定速度逼近目标的效果,具体代码如下,利用yield我们可以实现在飞行物未达到目标点的时候每帧执行一次的效果:

    ### 1. handle for missle skill(etc: fire ball)
    def missle_handle_func(skill_data, instance, user, skill_step, action_group, target_id, target_pos):
      effect = instance.create_effect(skill_step.missle_info.missle_fx_path)
      effect.set_scale(skill_step.missle_info.missle_scale)
    
    
      cur_target_pos, is_target_valid = skill_step.missle_info.get_target_position(user, target_id, target_pos)
      start_pos = skill_step.missle_info.get_start_position(user, target_id, target_pos)
    
    
      is_reach_target = False
      from common.gametime import GameTime
      init_time = GameTime.now_time
      while True:
        # some code ignore here
          # ...
        fly_distance = skill_step.missle_info.fly_speed*GameTime.elapse_time
    
    
        if fly_distance < total_distance:
          start_pos += fly_direction*math3d.vector(fly_distance, fly_distance, fly_distance)
          effect.set_position(start_pos)
        else:
          is_reach_target = True
          break
    
    
            # do yield util next frame
        yield
    
    
      effect.destroy()
    
    
      if is_reach_target:
        target_list = skill_data.get_target_list(user.caster, target_id, target_pos)
        for target in target_list:
          action_group.do(user.caster, target)
    • 子任务-引导类技能代码

    对于上面介绍的引导类技能(闪电链),依托框架本身的guide effect实现,我们利用yield TaskSleep()就能很好的完成相关的功能了:

    ### 2. handle for guide skill(etc: lighting chain)
    def guide_handle_func(skill_data, instance, user, skill_step, start_pos, target_id, target_pos):
      effect = instance.create_effect(skill_step.guide_info.guide_fx_path)
      effect.set_scale(skill_step.guide_info.guide_scale)
    
    
      effect.set_position(start_pos)
    
    
      effect.set_guide_end_pos(target_pos - start_pos)
    
    
        # yield for sleep
      yield TaskSleep(skill_step.guide_info.guide_time)
      effect.destroy()

    (二)对应的C++实现

    前面的python实现只是个引子,抛开具体的画面和细节,我们来尝试用我们构建的C++20版协程调度器来实现相似的代码(抛开显示相关的内容,纯粹过程模拟):

    //C++ 20 skill test coroutine
    mScheduler.CreateTask20([instance]() -> rstudio::logic::CoResumingTaskCpp20 {
        rstudio::logic::ISchedTask* task = rco_self_task();
        task->SetReturnFunction([](const rstudio::logic::CoReturnObject*) {
            //ToDo: return handle code add here
        });
    
    
        for (auto& skill_step : step_list)
        {
            auto step_start_time = GGame->GetTimeManager().GetTimeHardwareMS();
            switch (skill_step.cast_type)
            {
                case CastSkillStep::CAST_TYPE_PERIOD:
                    {
                        if (skill_step.cast_action_group_list.size() > 0)
                        {
                            auto& action_group = skill_step.cast_action_group_list[0];
                            for (int i = 0; i < skill_step.cast_count; i++)
                            {
                                co_await rstudio::logic::cotasks::Sleep(skill_step.cast_period);
                                bool ret_val = do_skill_spend(skill_data, user, instance);
                                if (!ret_val)
                                {
                                    co_return rstudio::logic::CoNil;
                                }
                                do_one_skill_cast(skill_data, skill_step, action_group, user, instance, target_id, target_pos);
                            }
                        }
                    }
                    break;
                case CastSkillStep::CAST_TYPE_MISSLE_TO_TARGET:
                    {
                        if (skill_step.cast_action_group_list.size() > 0)
                        {
                            auto& action_group = skill_step.cast_action_group_list[0];
                            for (int i = 0; i < skill_step.cast_count; i++)
                            {
                                co_await rstudio::logic::cotasks::Sleep(skill_step.cast_period);
                                bool ret_val = do_skill_spend(skill_data, user, instance);
                                if (!ret_val)
                                {
                                    co_return rstudio::logic::CoNil;
                                }
                                auto task_id = co_await rstudio::logic::cotasks::CreateTask(true, [&skill_step]()->rstudio::logic::CoResumingTaskCpp20 {
                                    auto cur_target_pos = skill_step.missle_info.get_target_position(user, target_id, target_pos);
                                    auto start_pos = skill_step.missle_info.get_start_position(user, target_id, target_pos);
    
    
                                    bool is_reach_target = false;
                                    auto init_time = GGame->GetTimeManager().GetTimeHardwareMS();
                                    auto last_time = init_time;
                                    do
                                    {
                                        auto now_time = GGame->GetTimeManager().GetTimeHardwareMS();
                                        auto elapse_time = now_time - last_time;
                                        last_time = now_time;
                                        if (now_time - init_time >= skill_step.missle_info.long_fly_time)
                                        {
                                            break;
                                        }
    
    
                                        auto cur_target_pos = skill_step.missle_info.get_target_position(user, target_id, target_pos);
    
    
                                        rstudio::math::Vector3 fly_direction = cur_target_pos - start_pos;
                                        auto total_distance = fly_direction.Normalise();
                                        auto fly_distance = skill_step.missle_info.fly_speed * elapse_time;
                                        if (fly_distance < total_distance)
                                        {
                                            start_pos += fly_direction * fly_distance;
                                        }
                                        else
                                        {
                                            is_reach_target = true;
                                            break;
                                        }
    
    
                                        co_await rstudio::logic::cotasks::NextFrame{};
                                    } while (true);
                                    if (is_reach_target)
                                    {
                                        //ToDo: add damage calculate here~~
                                    }
    
    
                                });
                                instance.add_child_task_id(task_id);
                            }
                        }
                    }
                    break;
                case CastSkillStep::CAST_TYPE_GUIDE_TO_TARGET:
                    {
                        //ignore here
                    }
                    break;
                default:
                    break;
            }
    
    
            auto now_time = GGame->GetTimeManager().GetTimeHardwareMS();
            auto step_pass_time = now_time - step_start_time;
            auto need_sleep_time = skill_step.step_total_time - step_pass_time;
            if (need_sleep_time > 0)
            {
                co_await rstudio::logic::cotasks::Sleep(need_sleep_time);
            }
    
    
            instance.on_one_step_finish(skill_step);
        }
    
    
        if (skill_data.delay_end_time > 0)
        {
            co_await rstudio::logic::cotasks::Sleep(skill_data.delay_end_time);
        }
    
    
        for (auto tid :instance.child_task_list)
        {
            co_await rstudio::logic::cotasks::WaitTaskFinish(tid, 10000);
        }
    });

    我们可以看到, 依赖C++20的新特性和我们自己封装的调度器, 我们已经可以很自然很顺畅的用比较低的心智负担来表达原来在python中实现的功能了, 这应该算是一个非常明显的进步了。

    (三)小结

    通过上面的例子我们可以看到协程化实现相关功能的好处:

    1. 不会形成大量的对象抽象,基于过程本身实现主体功能即可。

    2. 更容易写出数据驱动向的实现。

    3. 避免过多的Callback干扰主体逻辑实现。

    四、结语 

    综合来看,C++20提供了一套理解上稍显复杂,但不碍使用的一套协程方案,20版的移除了诸多限制,尤其是自动栈变量的处理,让业务侧可以用更低的心智负担来使用协程,从某种程度已经接近如上例中python这种脚本能够提供的编程体验了,也算是一种不错的进步吧。

    抛开机制本身的复杂度,以前文介绍的RpcRequest的协程化举例,相关的awaitable的实现比较套路化,也比较简单,所以形成了体系化的框架层后,实际的扩展和迭代并不复杂。对于异步操作比较多的场合,特别是多个异步操作混合,还是很值得实装的。

     作者简介

    e77652f901ddf34fece1bf6d974a6901.png

    沈芳

    腾讯光子R工作室后台开发工程师

    光子R工作室后台开发工程师,毕业于华中科技大学。目前负责SNGame项目后台主程工作。主要对GamePlay技术比较感兴趣。

     推荐阅读

    拒绝千篇一律,这套Go错误处理的完整解决方案值得一看!

    10个技巧!实现Vue.js极致性能优化(建议收藏)

    为什么WebAssembly不是JavaScript的终结者,而是它的“助推器”?

    快人一步掌握vue源码解读,搞定diff算法!(超详细)


    0b64ab258a0b15d80d5318b942a00185.gif

    展开全文
  • 协程coroutine.h

    2018-09-06 01:10:05
    开源的c++协程实现,包含头文件即可使用详见https://github.com/tonbit/coroutine
  • 协程 Coroutine

    2020-12-22 01:56:20
    协程 Coroutine A coroutine is a function that can suspend its execution (yield) until the given given YieldInstruction finishes. 感觉意思就是用户定义的伪多线程(同多线程对业务逻辑所需的功能特点)。...
  • 本文翻译自c++协程库cppcoro库作者Lewis Baker的github post,...Coroutine Theory This is the first of a series of posts on the C++ Coroutines TS, a new language feature that is currently on track for inclu

    本文翻译自c++协程库cppcoro库作者Lewis Baker的github post,本篇为第一篇,原文内容在https://lewissbaker.github.io/2017/09/25/coroutine-theory

    Coroutine Theory

    This is the first of a series of posts on the C++ Coroutines TS, a new language feature that is currently on track for inclusion into the C++20 language standard.

    这是关于C++协程系列的第一篇文章,C++协程是一个新的语言特性,它正在被纳入到C++20语言标准中。

    In this series I will cover how the underlying mechanics of C++ Coroutines work as well as show how they can be used to build useful higher-level abstractions such as those provided by the cppcoro library.

    在本系列中,我将介绍C++协程的底层机制如何工作以及如何使用它们来构建有用的更高级抽象,例如cppcoro库提供的抽象。

    In this post I will describe the differences between functions and coroutines and provide a bit of theory about the operations they support. The aim of this post is introduce some foundational concepts that will help frame the way you think about C++ Coroutines.

    这篇文章中我将介绍函数与协程的不同,也会提供它们所支持的一些操作的基础原理。这篇文章的目的是介绍一些帮助你理解C++协程的一些基础概念。

    Coroutines are Functions are Coroutines

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

    协程就是可以被挂起然后一段时间后再次被恢复执行的函数。

    I will explain what this means in a bit more detail, but before I do I want to first review how a “normal” C++ function works.

    我将在后面详细解释这句话,但是现在我想先回顾一下一个“一般”C++函数是怎么工作的。

    “Normal” Functions

    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.

    返回操作将返回值返回给主调函数,将(被调函数的)活跃帧销毁,然后恢复主调函数的执行。

    Let’s analyse these semantics a little more…

    让我们对上面一段话做一些深入分析。

    Activation Frames

    So what is this ‘activation frame’ thing?

    首先,什么是“活跃帧”?

    You can think of the activation frame as the block of memory that holds the current state of a particular invocation of a function. This state includes the values of any parameters that were passed to it and the values of any local variables.

    你可以将活跃帧认为是保存所调用函数的当前状态的一块儿内存。这个当前状态包括传入的参数和函数内的局部变量。

    For “normal” functions, the activation frame also includes the return-address - the address of the instruction to transfer execution to upon returning from the function - and the address of the activation frame for the invocation of the calling function. You can think of these pieces of information together as describing the ‘continuation’ of the function-call. ie. they describe which invocation of which function should continue executing at which point when this function completes.

    对于“一般”函数,活跃帧还包括返回地址,这个返回地址就是从这个一般函数返回时将执行转移到的指令的地址,也就是调用(此一般)函数的主调函数活跃帧的地址(译者注:这里作者想表达,A调用B,那么B的活跃帧包含B返回时继续执行A函数语句的地址)。你可以把这些信息视作继续执行原来调用这个一般函数的主调函数的所需要的信息。这些信息包括了当函数执行完成后应当继续执行从哪个函数的哪个断点执行。

    With “normal” functions, all activation frames have strictly nested lifetimes. This strict nesting allows use of a highly efficient memory allocation data-structure for allocating and freeing the activation frames for each of the function calls. This data-structure is commonly referred to as “the stack”.

    对于“一般”函数,所有活跃帧都有严格的嵌套的生命周期。这种严格的嵌套使得函数调用期间所发生的内存分配和释放可以用一个高效率的数据结构完成。这种数据结构就是“栈”。

    When an activation frame is allocated on this stack data structure it is often called a “stack frame”.

    当一个活跃帧被分配到栈上时,我们一般称其为“栈帧”。

    This stack data-structure is so common that most (all?) CPU architectures have a dedicated register for holding a pointer to the top of the stack (eg. in X64 it is the rsp register).

    栈这种数据结构非常常见,几乎每一种CPU架构都会留一个寄存器专门用于存储栈顶(比如在x64架构的处理器中就是rsp寄存器)。

    To allocate space for a new activation frame, you just increment this register by the frame-size. To free space for an activation frame, you just decrement this register by the frame-size.

    要为新的激活帧分配空间,只需按帧大小增加这个寄存器。要为激活帧释放空间,只需按帧大小递减该寄存器。

    The ‘Call’ Operation

    When a function calls another function, the caller must first prepare itself for suspension.

    当一个函数调用另一个函数时,主调函数必须首先准备将自己挂起。

    This ‘suspend’ step typically involves saving to memory any values that are currently held in CPU registers so that those values can later be restored if required when the function resumes execution. Depending on the calling convention of the function, the caller and callee may coordinate on who saves these register values, but you can still think of them as being performed as part of the Call operation.

    “挂起”操作包括将当前CPU寄存器中的所有值保存到内存中,这样当程序恢复执行主调函数时可以将这些数据从内存中加载回来。根据函数的调用约定,主调函数和被调函数可能会协商到底谁负责保存这些寄存器值,但是你仍可以认为这些保存操作是由调用操作完成的。

    The caller also stores the values of any parameters passed to the called function into the new activation frame where they can be accessed by the function.

    主调函数也会将传递给被调函数的参数保存到新的活跃帧中,这样被调函数就可以访问这些参数。

    Finally, the caller writes the address of the resumption-point of the caller to the new activation frame and transfers execution to the start of the called function.

    最后,主调函数将恢复执行主调函数的地址写入到新的活跃帧,然后将执行权转移给被调函数的开始。

    In the X86/X64 architecture this final operation has its own instruction, the call instruction, that writes the address of the next instruction onto the stack, increments the stack register by the size of the address and then jumps to the address specified in the instruction’s operand.

    在x86/x64架构CPU中,这最后的一步操作也有其对应的指令,即call指令,它会将(主调函数的)下一条指令的地址压到栈中,按该地址大小增加栈寄存器,然后跳转到指令操作数中指定的地址。

    The ‘Return’ Operation

    When a function returns via a return-statement, the function first stores the return value (if any) where the caller can access it. This could either be in the caller’s activation frame or the function’s activation frame (the distinction can get a bit blurry for parameters and return values that cross the boundary between two activation frames).

    当一个函数使用return语句返回时,它将会首先将返回值(如果有的话)存到主调函数可以访问到的地方。这个地方可能是主调函数的活跃帧,也可能是被调函数的活跃帧(对于两个函数活跃帧参数和返回值交界的情况,这个区别可能会有些模糊)。

    Then the function destroys the activation frame by:

    • Destroying any local variables in-scope at the return-point.
    • Destroying any parameter objects
    • Freeing memory used by the activation-frame

    然后函数将会通过以下方式销毁活跃帧:

    • 在返回点销毁作用域中的所有局部变量
    • 销毁所有参数
    • 回收活跃帧所占用的内存

    And finally, it resumes execution of the caller by:

    • Restoring the activation frame of the caller by setting the stack register to point to the activation frame of the caller and restoring any registers that might have been clobbered by the function.
    • Jumping to the resume-point of the caller that was stored during the ‘Call’ operation.

    最后,它通过以下方式恢复主调函数的执行:

    • 通过将栈寄存器设置为指向主调函数的活跃帧,并还原任何可能已被函数占用过的寄存器,来还原主调函数的激活帧
    • 跳转到主调函数在执行“Call”操作期间存储的恢复断点

    Note that as with the ‘Call’ operation, some calling conventions may split the repsonsibilities of the ‘Return’ operation across both the caller and callee function’s instructions.

    注意,有些调用约定的可能会将“Return”操作的责任分配给主调函数和被调函数两者来承担

    Coroutines

    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.

    Suspend操作在函数中的当前点暂停协程的执行,并转去执行主调函数或调用resume的函数,但它并不销毁当前函数的活跃帧。在协程执行被挂起的时候,所有当前挂起断点作用域内的对象仍然存在。

    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.

    注意,就像函数的Return操作一样,协程只能在定义良好的挂起点被本身挂起。

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

    Resume操作在挂起的协程的挂起点恢复对该协程的执行。这将重新激活协程的活跃帧。

    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.

    Destroy操作不恢复协程的执行直接将其活跃帧销毁,所有挂起点作用域内的对象都将会被销毁,存储活跃帧的内存也将被释放。

    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.

    由于协程可以在不被销毁活跃帧的情况下挂起,那么我们也无法保证活跃帧的生存周期将会是严格嵌套的。这意味着活跃帧无法再用栈这种数据结构存储而需要被保存在堆上。

    There are some provisions in the C++ Coroutines TS to allow the memory for the coroutine frame to be allocated from the activation frame of the caller if the compiler can prove that the lifetime of the coroutine is indeed strictly nested within the lifetime of the caller. This can avoid heap allocations in many cases provided you have a sufficiently smart compiler.

    在C++ Coroutines TS中有一些规定,允许编译器从主调函数的活跃帧分配内存,如果编译器可以证明协同程序的生存周期确实在主调函数的生存周期内严格嵌套的话。在许多情况下,只要有足够智能的编译器,这可以避免堆分配。

    With coroutines there are some parts of the activation frame that need to be preserved across coroutine suspension and there are some parts that only need to be kept around while the coroutine is executing. For example, the lifetime of a variable with a scope that does not span any coroutine suspend-points can potentially be stored on the stack.

    对于协程,活跃帧的有些部分需要在协程挂起的时候也要被保留,而另一些部分只需要在协程执行的时候保留。例如,生命周期不跨挂起点的变量将可能会被分配到栈上。

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

    你可以将活跃帧认为由逻辑上的两部分组成:“协程帧(coroutine frame)”和“栈帧(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.

    “协程帧”持有在协程挂起期间仍然存在的那部分协程活跃帧,而“栈帧”持有只在协程执行期间存在的那部分栈帧,“栈帧”的这部分内存会在协程挂起和将控制权交回给主调函数和调用Resume的函数时被释放。

    The ‘Suspend’ operation

    The Suspend operation of a coroutine allows the coroutine to suspend execution in the middle of the function and transfer execution back to the caller or resumer of the coroutine.

    协程的“Suspend”操作允许协程从函数中间暂停执行,而转去执行主调函数或调用了Resume的函数。

    There are certain points within the body of a coroutine that are designated as suspend-points. In the C++ Coroutines TS, these suspend-points are identified by usages of the co_await or co_yield keywords.

    协程体中有一些点被指定为挂起点。在C++ Coroutines TS中,这些挂起点通过co_await或co_yield关键字来标识。

    When a coroutine hits one of these suspend-points it first prepares the coroutine for resumption by:

    • Ensuring any values held in registers are written to the coroutine frame
    • Writing a value to the coroutine frame that indicates which suspend-point the coroutine is being suspended at.This allows a subsequent Resume operation to know where to resume execution of the coroutine or so a subsequent Destroy to know what values were in-scope and need to be destroyed.

    当一个协程执行到这些挂起点时它首先通过以下操作准备将来的恢复:

    • 保证所有的寄存器值都被写入到协程帧中
    • 向协程帧写入一个值,该值指示该协程被挂起的挂起点。这允许后续的恢复操作知道在何处恢复执行协程程序,或者允许后续的销毁操作知道哪些值在作用域内,哪些值需要销毁。

    Once the coroutine has been prepared for resumption, the coroutine is considered ‘suspended’.

    一旦协程准备好了被恢复,那么该协程被认为处于“挂起”状态

    The coroutine then has the opportunity to execute some additional logic before execution is transferred back to the caller/resumer. This additional logic is given access to a handle to the coroutine-frame that can be used to later resume or destroy it.

    然后,协程有机会在执行权被转回调用者/恢复者之前执行一些额外的逻辑。这个额外的逻辑被赋予了对协程帧句柄的访问权,该句柄可用于以后恢复或销毁它。

    This ability to execute logic after the coroutine enters the ‘suspended’ state allows the coroutine to be scheduled for resumption without the need for synchronisation that would otherwise be required if the coroutine was scheduled for resumption prior to entering the ‘suspended’ state due to the potential for suspension and resumption of the coroutine to race. I’ll go into this in more detail in future posts.

    这种在协程进入“挂起”状态后执行逻辑的能力允许协程被安排为恢复,而不需要同步,如果协程在进入“挂起”状态之前被安排为恢复,那么则需要同步,以防止可能出现的挂起和恢复的竞态条件。我将在以后的帖子中更详细地讨论这个问题。

    The coroutine can then choose to either immediately resume/continue execution of the coroutine or can choose to transfer execution back to the caller/resumer.

    协程程序可以选择立即恢复/继续执行协程或者将执行权交回给主调函数/恢复者。

    If execution is transferred to the caller/resumer the stack-frame part of the coroutine’s activation frame is freed and popped off the stack.

    如果执行权被交回给主调函数/恢复者,那么协程活跃帧的栈帧部分将会被释放和出栈。

    The ‘Resume’ operation

    The Resume operation can be performed on a coroutine that is currently in the ‘suspended’ state.

    可以在一个“挂起”状态的协程上执行Resume操作。

    When a function wants to resume a coroutine it needs to effectively ‘call’ into the middle of a particular invocation of the function. The way the resumer identifies the particular invocation to resume is by calling the void resume() method on the coroutine-frame handle provided to the corresponding Suspend operation.

    当一个函数要对协程执行resume操作时,它需要有效地“调用”协程函数内的代码。resumer要恢复的特定调用的方法是,对相应Suspend操作提供的协程帧句柄调用void resume() 方法。

    Just like a normal function call, this call to resume() will allocate a new stack-frame and store the return-address of the caller in the stack-frame before transferring execution to the function.

    就像一个一般的函数调用一样,对resume的调用也会分配一个新的栈帧,并且会在执行权交出之前将主调函数的返回地址存储在栈帧上。

    However, instead of transferring execution to the start of the function it will transfer execution to the point in the function at which it was last suspended. It does this by loading the resume-point from the coroutine-frame and jumping to that point.

    然而,resume方法并不会像一般执行函数那样跳转到被调函数的开始位置执行,它会转到协程程序被挂起时执行到的位置继续执行。它通过从协程帧中加载resume-point并且跳转到这个执行位置来实现这种(从函数中间开始执行)。

    When the coroutine next suspends or runs to completion this call to resume() will return and resume execution of the calling function.

    当协程程序下一次挂起或者完成时,这一次对resume的调用将会返回,然后将会转回继续执行(调用了resume的)主调函数。

    The ‘Destroy’ operation

    The Destroy operation destroys the coroutine frame without resuming execution of the coroutine.

    “Destroy”操作将不通过恢复协程的执行而直接将其协程帧销毁掉。

    This operation can only be performed on a suspended coroutine.

    这个操作只能作用于一个挂起的协程上。

    The Destroy operation acts much like the Resume operation in that it re-activates the coroutine’s activation frame, including allocating a new stack-frame and storing the return-address of the caller of the Destroy operation.

    “Destroy”操作与“Resume”操作在某种意味上有一些相似之处,它也会激活协程的活跃帧,包括分配一个新的栈帧然后存储调用了“Destroy”的函数的返回地址。

    However, instead of transferring execution to the coroutine body at the last suspend-point it instead transfers execution to an alternative code-path that calls the destructors of all local variables in-scope at the suspend-point before then freeing the memory used by the coroutine frame.

    但是,它没有在最后一个挂起点将执行转移到协程体,而是将执行转移到另一个代码路径,该路径在挂起点调用作用域中所有局部变量的析构函数,然后释放协程帧使用的内存。

    Similar to the Resume operation, the Destroy operation identifies the particular activation-frame to destroy by calling the void destroy() method on the coroutine-frame handle provided during the corresponding Suspend operation.

    与Resume操作类似,Destroy操作通过在相应的Suspend操作期间提供的协程帧句柄上调用void Destroy()方法来销毁特定的活跃帧。

    The ‘Call’ operation of a coroutine

    The Call operation of a coroutine is much the same as the call operation of a normal function. In fact, from the perspective of the caller there is no difference.

    协程程序的“调用”和一般函数的调用基本相同。实际上,从主调函数的角度两者没有差异。

    However, rather than execution only returning to the caller when the function has run to completion, with a coroutine the call operation will instead resume execution of the caller when the coroutine reaches its first suspend-point.

    然而,除了在函数执行到结尾时会返回执行主调函数外,协程程序也会在它执行到第一个挂起点时返回继续执行主调函数。

    When performing the Call operation on a coroutine, the caller allocates a new stack-frame, writes the parameters to the stack-frame, writes the return-address to the stack-frame and transfers execution to the coroutine. This is exactly the same as calling a normal function.

    当“调用”一个协程程序时,主调函数分配一个新的栈帧,将参数写入到栈帧上,将返回地址写入到栈帧上,然后转去执行协程程序。这与调用一个一般函数完全相同。

    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.

    协程程序所做的第一件事就是在堆上分配一个协程帧,然后将栈帧上的数据复制或移动到协程帧上,这样这些数据的生存周期就可以延伸到第一个挂起点之后。

    The ‘Return’ operation of a coroutine

    The Return operation of a coroutine is a little different from that of a normal function.

    协程程序的“返回”则与一般函数有所不同。

    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).

    当一个协程程序执行到return语句(在TS中是co_return语句)时,它将返回值存储在某一个地方(具体的位置要看不同编译器对协程的实现),之后将本地作用域内的变量全部销毁(不包括参数)。

    The coroutine then has the opportunity to execute some additional logic before transferring execution back to the caller/resumer.

    之后协程程序有一次在将执行权交回给主调函数/恢复函数之前执行一些额外逻辑的机会。

    This additional logic might perform some operation to publish the return value, or it might resume another coroutine that was waiting for the result. It’s completely customisable.

    可以通过额外逻辑来发布返回值,或者恢复另一个等待此执行结果的协程。这完全由你定制。

    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.

    然后根据Suspend/Destroy操作语义将执行权转移回调用者/恢复者,将活跃帧数据弹出堆栈。

    It is important to note that the return-value passed to the Return operation is not the same as the return-value returned from a Call operation as the return operation may be executed long after the caller resumed from the initial Call operation.

    需要强调的是,传递给“返回”操作的返回值与传递给“调用”操作的返回值不相同,因为返回操作可能在主调函数从初始的“调用”操作恢复执行后很长时间才被执行。

    An illustration

    To help put these concepts into pictures, I want to walk through a simple example of what happens when a coroutine is called, suspends and is later resumed.

    为了更好地说明这些概念,我想串一个例子来说明当协程被调用、挂起和被恢复时发生了什么。

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

    假设我们有一个函数(或协程)f(),调用了另外一个协程x(int a)。

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

    在调用之前,情况可能如下图所示:
    在这里插入图片描述
    Then when x(42) is called, it first creates a stack frame for x(), as with normal functions.

    然后当调用了x(42)时,像一般函数一样,它首先会为x()函数创建一个栈帧。

    在这里插入图片描述
    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).

    然后,一旦协程x()在堆上为协程帧分配了内存并且将参数值复制或移动到协程帧上,那么最终结果将会如下图所示。注意编译器将会将协程帧的地址放到一个与栈指针不同的寄存器内(在MSVC中它是rbg寄存器)。

    在这里插入图片描述
    If the coroutine x() then calls another normal function g() it will look something like this.

    如果协程x()调用了另外的一个一般函数g(),那么情况就会变得有点像这样。

    在这里插入图片描述
    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.

    当g()返回时它将会销毁g()的活跃帧然后恢复x()的活跃帧。假设g()的返回值存储在活跃帧的局部变量b中。

    在这里插入图片描述
    If x() now hits a suspend-point and suspends execution without destroying its activation frame then execution returns to f().

    如果此时x()执行到第一个挂起点并且暂停了执行,但它没有销毁变量,它将会将执行权交还给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).

    这将会导致x()的栈帧部分出栈,但是它的协程帧部分仍然保存在堆上。当协程第一次挂起时,一个返回值将会返回给主调函数。这个返回值往往也持有一个协程帧挂起的句柄,这样以便于后面重新恢复它。当x()挂起时它也会将x()的恢复执行的地址存储到协程帧上(一般将恢复点简写为RP)。

    在这里插入图片描述
    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.

    这个句柄现在可以像一个一般的值一样在函数间传递。在之后的一些位置,可能是来自于不同的调用栈,甚至是不同的线程,假设是h()函数,决定恢复协程的执行。例如,这可能在一个异步I/O操作完成时出现。

    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.

    要恢复协程的函数通过调用 void resume(handle) 函数恢复了协程的执行。对于这个resume的主调函数,这次调用就像对一般的函数调用一样,调用了一个单参数的返回值类型为void的函数。

    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.

    这个操作创建了一个新的栈帧,这个栈帧记录了返回主调函数的地址以备后续返回,然后通过将被调函数x()的协程帧地址加载到寄存器中并开始执行协程帧中x()的恢复位置来激活协程帧。
    在这里插入图片描述

    In summary

    I have described coroutines as being a generalisation of a function that has three additional operations - ‘Suspend’, ‘Resume’ and ‘Destroy’ - in addition to the ‘Call’ and ‘Return’ operations provided by “normal” functions.

    这篇文章中我描述了由函数向协程扩展后的三个操作:“挂起”、“恢复”和“销毁”,以及“一般”函数的“调用”和“返回”操作。

    I hope that this provides some useful mental framing for how to think of coroutines and their control-flow.

    我希望这能为如何考虑协同过程及其控制流提供一些有用的思想框架。

    In the next post I will go through the mechanics of the C++ Coroutines TS language extensions and explain how the compiler translates code that you write into coroutines.

    在下一篇文章中,我将介绍C++ Coroutines TS语言扩展的机制,并解释编译器如何将编写的代码翻译成协程程序。

    展开全文
  • PYTHON 之 COROUTINE

    2019-08-12 22:20:11
    5. coroutine.send(None) 终止 averager 子生成器,子生成器抛出 StopIteration 异常并将返回的数据包含在异常对象的value中,yield from 可以直接抓取 StopItration 异常并将异常对象的 value 赋值给 results[key] ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 22,161
精华内容 8,864
关键字:

coroutine