VS2019 C++20的coroutines
01 协程(coroutines)
协程就是用户态线程内可以被异步执行的函数。用来在用户态下解决异步问题。
普通函数:每次调用只能从第一条语句开始执行直到某个出口结束。
协程函数:协程函数交出控制权后,可以再次从交出控制权的下一语句开始执行。
在协程的叫法出现以前,处理异步问题一般会使用操作系统提供的系统级API来解决异步问题。系统级的API都是大多采用回调函数方式实现。后来人们觉得使用异步回调形式的API比较麻烦,就开始提供异步调用库或者语言级支持。并且起了个优雅的名字–协同程序。
协程不是线程。协程包含在线程内。协程在用户态,由用户控制。协程的切换比线程切换效率高。
同一个线程在一个时间点最多只能跑一个协程;在同一个线程中,协程的运行是穿行的[穿插运行(同一个函数内非顺序运行),一般认为不同与串行(顺序运行)]。所以没有数据争用(data race),也不需要锁。
原则上,应该尽量避免将同一个协程的主体,放到不同的线程中同时执行。因为这样有很大概率发生数据争用(data race)。[其实这种情况下就是线程的数据争用问题]。所以我们应该在线程中讨论协程;而不是在进程中讨论协程。
- Boost.Fiber
- Boost.Coroutine
- fiber
- libco
- libtask
- …
协程根据实现方式不同,分为有栈协程(stackful coroutine)和无栈协程(stackless coroutine)。
有栈协程可通过操作系统提供的系统调用实现;无栈协程需要语言标准和编译器支持。
OS | 有栈协程系统调用 |
---|---|
Linux | getcontext,setcontext,makecontext,swapcontext |
Windows | CreateFiber,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_await、co_yield、co_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
就必须实现Awaitable
。co_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
并且在代码中加入实验库#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协程设计及实现