C++20 Coroutines 协程

01 协程(coroutines)

协程就是用户态线程内可以被异步执行的函数。用来在用户态下解决异步问题。

普通函数:每次调用只能从第一条语句开始执行直到某个出口结束。
协程函数:协程函数交出控制权后,可以再次从交出控制权的下一语句开始执行。

在协程的叫法出现以前,处理异步问题一般会使用操作系统提供的系统级API来解决异步问题。系统级的API都是大多采用回调函数方式实现。后来人们觉得使用异步回调形式的API比较麻烦,就开始提供异步调用库或者语言级支持。并且起了个优雅的名字–协同程序

协程不是线程。协程包含在线程内。协程在用户态,由用户控制。协程的切换比线程切换效率高。
同一个线程在一个时间点最多只能跑一个协程;在同一个线程中,协程的运行是穿行的[穿插运行(同一个函数内非顺序运行),一般认为不同与串行(顺序运行)]。所以没有数据争用(data race),也不需要锁。
原则上,应该尽量避免将同一个协程的主体,放到不同的线程中同时执行。因为这样有很大概率发生数据争用(data race)。[其实这种情况下就是线程的数据争用问题]。所以我们应该在线程中讨论协程;而不是在进程中讨论协程。

目前在C/C++中比较知名的协程(类协程)库有:
  • Boost.Fiber
  • Boost.Coroutine
  • fiber
  • libco
  • libtask

协程根据实现方式不同,分为有栈协程(stackful coroutine)无栈协程(stackless coroutine)
有栈协程可通过操作系统提供的系统调用实现;无栈协程需要语言标准和编译器支持。

OS有栈协程系统调用
Linuxgetcontext,setcontext,makecontext,swapcontext
WindowsCreateFiber,ConvertFiberToThread,SwitchToFiber

微软拟提的C++20标准中(目前是ts,即:<experimental/coroutine>)的协程属于stackless coroutine。

协程函数特点:

1:首次调用协程函数,会从堆中分配一个协程上下文,调用方的返回地址、入口函数、交出控制权等信息保存在协程上下文中。

2:当协程中途交出控制权后,协程上下文不会被删除(相当于函数退出之后,上下文环境还被保存,类比线程切换)。

3:当协程再次获得控制权后,会自动从协程上下文中恢复调用环境,然后从上一次交出控制权的下一条语句继续执行(加载目标协程环境,类比线程切换)。

4:协程函数返回(非中途交出控制权)后,协程上下文将被删除。

5:若再次调用协程函数,视为首次调用。

有栈协程(Stackful Coroutines)特点:

1:每个协程都有一个预先分配的调用栈(Call Stack)。

2:每个协程都属于且仅属于创建它的线程。

3:一个线程可以包含多个协程。

4:线程本身也可以是一个协程,成为主协程(Primary Coroutine)。

5:协程必须主动交出控制权,否则同一线程的其它协程均无法获得执行机会。

6:协程执行路径上,任何被调用的函数均可在任何位置交出控制权。

7:如果允许协程把控制权交给同一线程的其它协程,则称为对称协程(Symmetry Coroutines)。如果只允许协程把控制权交给主协程,主协程作为调度器,负责分配执行权,则称为非对称协程(Asymmetry Coroutines)。

8:属于同一线程的多个协程之间没有数据争用(Data Race)问题。

9:无需修改语言标准和编译器,利用系统调用即可实现。

无栈协程(Stackless Coroutines)特点:

1:每个协程的执行环境,仅需包含调用栈(Call Stack)的顶层栈帧(Top Call Stack Frame),而非整个调用栈,因而空间开销极小。

2:协程执行路径上,只有特定语句才能交出控制权。

3:无需协程调度器。

4:调用协程函数时,同一协程函数的不同部分,有可能在不同的线程环境中执行。因此需要处理好数据争用(Data Race)问题。这个问题实际是线程间数据争用问题。

5:需要语言标准和编译器支持。

有栈协程和无栈协程对比,有栈协程的最大缺陷是保存调用栈的开销大,尤其协程较多且切换频繁时;
无栈协程不但具有有栈协程的所有优点,而且空间开销极低;唯一不足就是需要语言标准和编译器支持。

怎么识别C++20中的协程

如果在C++20的一个函数体内包含co_awaitco_yieldco_return中任何一个关键字,那么这个函数就是一个coroutine。其中:

co_await:挂起当前的coroutine。
co_return:从当前coroutine返回一个结果。
co_yield:返回一个结果并且挂起当前的coroutine。

一个coroutine必定包含Promise和Awaitable两个部分。

协程通过Promise和Awaitable接口来规范实现。实现最简单的协程需要用到其中的8个(5个Promise的函数和3个Awaitable的函数)。

如果要实现形如co_await xxxxx;的协程调用格式, xxxxx就必须实现Awaitableco_await是一个新的运算符。Awaitable主要有3个函数:

await_ready:返回Awaitable实例是否已经ready。协程开始会调用此函数,如果返回true,表示你想得到的结果已经得到了,协程不需要执行了。所以大部分情况这个函数的实现是要return false。

await_suspend:挂起awaitable。该函数会传入一个coroutine_handle类型的参数。这是一个由编译器生成的变量。在此函数中调用handle.resume(),就可以恢复协程。

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

02 C++20 Coroutine Demo

启用ts中的Coroutine功能,需要vs2019中做如下配置:

配置属性==>常规==>C++语言标准,预览-最新C++工作草案中的功能(std:c++latest)
配置属性==>C/C++==>命令行,添加/await

C++20 Coroutine配置设置
C++20 添加/await,支持coroutine
并且在代码中加入实验库#include <experimental/coroutine>

下面的demo代码收集在:
https://github.com/5455945/cpp_demo/tree/master/C%2B%2B20

02.01 coroutine demo01

这是 https://github.com/franktea/temp/blob/master/uncategorized/coroutine.md 中实现的。在这里对每个函数断点后,调试,对理解协程流程很有帮助。再结合C++ 协程介绍[译]中的图形解释,会很有帮助。

// https://github.com/franktea/temp/blob/master/uncategorized/co_vs_callback.cpp
// https://github.com/franktea/temp/blob/master/uncategorized/coroutine.md
#include <iostream>
#include <thread>
#include <experimental/coroutine>
#include <chrono>
#include <functional>

#include "co_vs_callback.h"

 // clang++ -std=c++2a -fcoroutines-ts -lstdc++ co_vs_callback.cpp

using call_back = std::function<void(int)>;
void Add100ByCallback(int init, call_back f) // 异步调用
{
	std::thread t([init, f]() {
		std::this_thread::sleep_for(std::chrono::seconds(5));
		std::cout << "Add100ByCallback: " << init << std::endl;
		f(init + 100);
		});
	t.detach();
}

struct Add100Awaitable
{
	Add100Awaitable(int init) :init_(init) {}
	bool await_ready() const { return false; }
	int await_resume() { return result_; }
	void await_suspend(std::experimental::coroutine_handle<> handle)
	{
		auto f = [handle, this](int value) mutable {
			result_ = value;
			handle.resume();
		};
		Add100ByCallback(init_, f); // 调用原来的异步调用
	}
	int init_;
	int result_;
};

struct Task
{
	struct promise_type {
		auto get_return_object() { return Task{}; }
		auto initial_suspend() { return std::experimental::suspend_never{}; }
		auto final_suspend() { return std::experimental::suspend_never{}; }
		void unhandled_exception() { std::terminate(); }
		void return_void() {}
	};
};

Task Add100ByCoroutine(int init, call_back f)
{
	int ret = co_await Add100Awaitable(init);
	//ret = co_await Add100Awaitable(ret);
	//ret = co_await Add100Awaitable(ret);
	f(ret);
}

void co_vs_callback()
{
	//Add100ByCallback(5, [](int value) { std::cout << "get result: " << value << "\n"; });
	Add100ByCoroutine(10, [](int value) { std::cout << "get result from coroutine1: " << value << "\n"; });
	//Add100ByCoroutine(20, [](int value) { std::cout << "get result from coroutine2: " << value << "\n"; });
	//Add100ByCoroutine(30, [](int value) { std::cout << "get result from coroutine3: " << value << "\n"; });
	//Add100ByCoroutine(40, [](int value) { std::cout << "get result from coroutine4: " << value << "\n"; });
	std::this_thread::sleep_for(std::chrono::seconds(50));
}

02.02 Coroutine demo02 co_await

这个demo来自黑山是不是山黑C++ coroutine

// https://www.cnblogs.com/heishanlaoy/p/11760368.html
#include "co_vs_await.h"
#include <iostream>
#include <experimental/coroutine>
using namespace std;

template<class T>
struct test {
    // inner types
    struct promise_type;
    using handle_type = std::experimental::coroutine_handle<promise_type>; //type alias

                                                                           // functions
    test(handle_type h) :handle(h) { cout << "# Created a Test object\n"; }
    test(const test& s) = delete;
    test& operator=(const test&) = delete;
    test(test&& s) :handle(s.handle) { s.handle = nullptr; }
    test& operator=(test&& s) { handle = s.handle; s.handle = nullptr; return *this; }
    ~test() { cout << "#Test gone\n"; if (handle) handle.destroy(); }

    T get()
    {
        cout << "# Got return value\n";
        if (!(this->handle.done()))
        {
            handle.resume();  //resume
            return handle.promise().value;
        }
    }

    struct promise_type
    {
        promise_type() { cout << "@ promise_type created\n"; }
        ~promise_type() { cout << "@ promise_type died\n"; }

        auto get_return_object() //get return object
        {
            cout << "@ get_return_object called\n";
            return test<T>{handle_type::from_promise(*this)};// pass handle to create "return object"
        }

        auto initial_suspend() // called before run coroutine body
        {
            cout << "@ initial_suspend is called\n";
            // return std::experimental::suspend_never{}; // dont suspend it
            return std::experimental::suspend_always{};
        }

        auto return_void() // called when just before final_suspend, conflict with return_value
        {
            cout << "@ return_void is called\n";
            return std::experimental::suspend_never{}; // dont suspend it
                                                       //return std::experimental::suspend_always{};
        }

        auto yield_value(int t) // called by co_yield()
        {
            std::cout << "yield_value called\n";
            value = t;
            return std::experimental::suspend_always{};
        }

        auto final_suspend() // called at the end of coroutine body
        {
            cout << "@ final_suspend is called\n";
            return std::experimental::suspend_always{};
        }

        void unhandled_exception() //exception handler
        {
            std::exit(1);
        }

        //T await_transform() {}

        // data
        T value;
    };

    // member variables
    handle_type handle;
};

struct AwaiableObj
{
    int a;
    AwaiableObj() :a(0) {}
    bool await_ready()
    {
        cout << "@@ await_ready called\n";
        return true;
    }

    auto await_suspend(std::experimental::coroutine_handle<> awaiting_handle)
    {
        cout << "@@ await_suspend called\n";
        // return ;
        // return true;
        return false;
        // return awaiting_handle;
    }

    auto await_resume()
    {
        cout << "@@ await_resume called\n";
        return a++;
    }
};

test<int> await_routine()
{
    auto a = AwaiableObj{};
    for (int i = 0; i < 5; i++)
    {
        auto v = co_await a;
        co_yield v;
    }
}

void co_vs_await()
{
    auto a = await_routine();
    auto b = a.get();
    cout << "value is " << b << endl;
    b = a.get();
    cout << "value is " << b << endl;
    b = a.get();
    cout << "value is " << b << endl;
    b = a.get();
    cout << "value is " << b << endl;
    b = a.get();
    cout << "value is " << b << endl;
}

02.03 Coroutine demo02 co_return

这个demo来自黑山是不是山黑C++ coroutine

// https://www.cnblogs.com/heishanlaoy/p/11760368.html
#include "co_vs_return.h"
#include <iostream>
#include <experimental/coroutine>
using namespace std;

template<class T>
struct test {
    // inner types
    struct promise_type;
    using handle_type = std::experimental::coroutine_handle<promise_type>; //type alias

    // functions
    test(handle_type h) :handle(h) { cout << "# Created a Test object\n"; }
    test(const test& s) = delete;
    test& operator=(const test&) = delete;
    test(test&& s) :handle(s.handle) { s.handle = nullptr; }
    test& operator=(test&& s) { handle = s.handle; s.handle = nullptr; return *this; }
    ~test() { cout << "#Test gone\n"; if (handle) handle.destroy(); }

    T get()
    {
        cout << "# Got return value\n";
        if (!(this->handle.done()))
        {
            handle.resume();  //resume
            return handle.promise().value;
        }
    }

    struct promise_type
    {
        promise_type() { cout << "@ promise_type created\n"; }
        ~promise_type() { cout << "@ promise_type died\n"; }

        auto get_return_object() //get return object
        {
            cout << "@ get_return_object called\n";
            return test<T>{handle_type::from_promise(*this)};// pass handle to create "return object"
        }

        auto initial_suspend() // called before run coroutine body
        {
            cout << "@ initial_suspend is called\n";
            // return std::experimental::suspend_never{}; // dont suspend it
            return std::experimental::suspend_always{};
        }

        auto return_value(T v) // called when there is co_return expression
        {
            cout << "@ return_value is called\n";
            value = v;
            return std::experimental::suspend_never{}; // dont suspend it
                                                       //return std::experimental::suspend_always{};
        }

        auto final_suspend() // called at the end of coroutine body
        {
            cout << "@ final_suspend is called\n";
            return std::experimental::suspend_always{};
        }

        void unhandled_exception() //exception handler
        {
            std::exit(1);
        }

        // data
        T value;
    };

    // member variables
    handle_type handle;
};
test<int> return_coroutine()
{
    std::cout << "start return_coroutine\n";
    co_return 1;
    co_return 2; // will never reach here
}

void co_vs_return()
{
    auto a = return_coroutine();
    cout << "created a corutine, try to get a value\n";
    int an = a.get();
    cout << "value is " << an << endl;
    an = a.get();
    cout << "value is " << an << endl;
}

02.04 Coroutine demo02 co_yield

这个demo来自黑山是不是山黑C++ coroutine

// https://www.cnblogs.com/heishanlaoy/p/11760368.html
#include "co_vs_yield.h"
#include <iostream>
#include <experimental/coroutine>
using namespace std;

struct test {
    // inner types
    struct promise_type;
    using handle_type = std::experimental::coroutine_handle<promise_type>; //type alias

                                                                           // functions
    test(handle_type h) :handle(h) { cout << "# Created a Test object\n"; }
    test(const test& s) = delete;
    test& operator=(const test&) = delete;
    test(test&& s) :handle(s.handle) { s.handle = nullptr; }
    test& operator=(test&& s) { handle = s.handle; s.handle = nullptr; return *this; }
    ~test() { cout << "#Test gone\n"; if (handle) handle.destroy(); }

    int current_value() {
        return handle.promise().value;
    }

    bool move_next()
    {
        handle.resume();
        return !handle.done();
    }

    struct promise_type
    {
        promise_type() { cout << "@ promise_type created\n"; }
        ~promise_type() { cout << "@ promise_type died\n"; }

        auto get_return_object() //get return object
        {
            cout << "@ get_return_object called\n";
            return test{ handle_type::from_promise(*this) };// pass handle to create "return object"
        }

        auto initial_suspend() // called before run coroutine body
        {
            cout << "@ initial_suspend is called\n";
            return std::experimental::suspend_never{}; // dont suspend it
            //return std::experimental::suspend_always{};
        }

        auto return_void() // called when just before final_suspend, conflict with return_value
        {
            cout << "@ return_void is called\n";
            return std::experimental::suspend_never{}; // dont suspend it
                                                       //return std::experimental::suspend_always{};
        }

        auto yield_value(int t) // called by co_yield()
        {
            std::cout << "yield_value called\n";
            value = t;
            return std::experimental::suspend_always{};
        }

        auto final_suspend() // called at the end of coroutine body
        {
            cout << "@ final_suspend is called\n";
            return std::experimental::suspend_always{};
        }

        void unhandled_exception() // exception handler
        {
            std::exit(1);
        }

        // data
        int value;
    };

    // member variables
    handle_type handle;
};
test yield_coroutine(int count)
{
    std::cout << "start yield_coroutine\n";
    for (int i = 0; i < count; i++)
        co_yield i * 2;
}

void co_vs_yield()
{
    auto a = yield_coroutine(4);
    cout << "created a corutine, try to get a value\n";
    do
    {
        cout << "get value " << a.current_value() << endl;
    } while (a.move_next());
}

02.05 librf库

librf是一个基于C++ Coroutines提案 ‘Stackless Resumable Functions’编写的非对称stackless协程库。
https://github.com/tearshark/librf

参考

1.C++ 协程介绍[译]
2.C++协程
3.C++ coroutine
4.co_vs_callback
5.isocpp.org的experimental
6.万字长文 | 漫谈libco协程设计及实现

  • 9
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值