一、协程
在谷歌的Golang中,如果大家说他的特点有啥,肯定绕不过协程。而在此之前,大多数的语言一般是从多进程讲到多线程,一般来说,对某个语言掌握的深度,就看在多线程下编程的能力(当然,没有多线程的除外)。多线程的编程难度自然是很多程序员望而却步的。虽然说多线程编程有他的优势,但能不能一种更好的方式,既可以有多线程的并行执行的优势,又能降低其开发的难度呢?于是在Go语言中出来的协程。
协程可以理解为用户态的线程,大家都知道,多线程的调度是由内核来完成的。所以在多线程编程中,会涉及到相当多的状态保护和资源保护。同时,为了保证公平,内核又采用了多种CPU分片的调度方法。可是当线程非常多时,不停的上下文切换会浪费大量宝贵的时间。这也是在多线程编程中的一个老大难问题,到底创建多少线程为宜?于是各种线程池技术也都引入进来,用各种手段(比如核的2倍等)来保证线程的切换相对来说最好。
c++20的协程是无栈协程,但是目前看来,其使用有一定复杂性,不过在其它一些封装好的库中,使用就相当方便了。估计标准库中封装好的协程,可能会推迟到c++23标准中去了。由于协程将调度方式和资源控制交还给了用户,那么线程的切换的缺点,就得以相当程度的避免了。但需要注意的是,谈协程仍然离不开线程,协程是运行在线程之上的,只是用户不需要在直接管理线程,同一个线程在同一时刻只能运行一个协程,但在同一个线程中,协程是串行的,不会产生线程资源的竞争(data race),所以原则上说,是不需要加锁的。
这也是为什么在文档中建议不要把同一个协程的运行块放置到不同的线程中运行的原因,因为如果这样,那么就极可能会发生资源竞争,从某种意义上说,回到了线程编程的方式。
在c++20的协程提交过程中,谷歌为了和微软竞争,提交了一个差不太多的提议,有点不厚道的味道,不过最终还是微软的提议胜出了,看来谷歌有点趋向保守的意思?
二、c++20协程应用场景和特点
协程其是就是搞定异步而生的。在以前,异步(事件驱动、异步IO和数据异步)的实现,基本有两个手段,一个是使用多线程+回调函数,另外一个就需要内核的API,在内部通过中断+回调来实现。如果有过异步多线程编程经验的程序员理解这个就非常容易了。可是,在这种编程中的一个缺点是,你得有两种编程理解,一种是同步的,所调即所得;一种是异步的,你得等人家回调你,才会得到想要的结果。这对于一些编程新手和对异步编程不太熟悉的程序员来说,其实是很痛苦的。那么协程就解决了这个问题,在上层,体现为所调即所得,在底层,会不断的等待运行结果的完成。
c++20的协程的特点有以下几个:
1、不需要内部栈分配,仅需要一个调用栈的顶层桢。
2、协程运行过程中,需要使用关键词来控制运行过程(比如co_return)。
3、协程可能分配不同线程,触发资源竞争。
4、没有调度器,但是需要标准和编译器的支持。
三、配置
协程的使用,需要尽量升级编译器到新版本。在Windows平台建议使用VS2019,在Linux平台建议使用GCC10.1以上,并开启c++20选项。这次为了测试方便,使用的是VS2019,并升级到了16.7.3,但是应该从16.6就开始支持了。为了能够支持协程,需要进行如下配置:
然后在“属性——c/c++——所有选项”中查看:
四、应用
下面看几个小例子,这些例子都是自带或者官网的一部分,也有一部分是从相关的资源网站获得,此处不再一一列举出处:
#include <iostream>
#include <thread>
#include <experimental/coroutine>
#include <chrono>
#include <functional>
// coroutine.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(1));
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)
{
std::cout << "call coroutine----1" << std::endl;
int ret = co_await Add100Awaitable(init);
std::cout << "call coroutine----1 yyyy:" <<ret<< std::endl;
co_return ;
ret = co_await Add100Awaitable(ret);
std::cout << "call coroutine----1 xxxxx:" << ret << std::endl;
//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(20));
}
//co_return.cpp
#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;
}
//test_coroutin.cpp
#include <vector>
#include <chrono>
#include <future>
#include <string>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <experimental/coroutine>
#include <experimental/generator>
#include <time.h>
namespace coroutines = std::experimental;
namespace hey_coroutines
{
using namespace std::chrono_literals;
// generator<T> 延迟求值
coroutines::generator<int> lazy_get_range(int min, int max)
{
for (int i = min; i <= max; i++)
{
co_yield i;
}
}
std::future<std::vector<std::string>> heavy_code_async()
{
return std::async([]
{
std::vector<std::string> result;
// 模拟耗时任务
for (size_t i = 100; i <= 500; i += 100)
{
result.emplace_back("我是:" + std::to_string(i));
std::this_thread::sleep_for(1s);
}
return result;
});
}
std::future<void> invoke_heavy_code()
{
for (auto& item : co_await heavy_code_async())
{
std::cout << item << std::endl;
}
}
void CallTest()
{
std::vector<int> numbers;
// 延迟求值
auto range = hey_coroutines::lazy_get_range(0, 10);
// 实际求值
std::copy(std::begin(range), std::end(range), std::back_inserter(numbers));
for (auto& item : numbers)
{
std::cout << item << std::endl;
}
// 模拟异步耗时任务
std::cout << "请等待协程执行完毕..." << std::endl;
// 由于只是例子,就粗暴等待。。实际应用肯定是不会阻塞主线程执行的。
hey_coroutines::invoke_heavy_code().wait();
}
std::experimental::generator<int> GetSequenceGenerator(
int startValue,
size_t numberOfValues) {
for (int i = 0 ; i < startValue + numberOfValues; ++i) {
time_t t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
char str[600];
ctime_s(str, 600, &t);
std::cout << str << std::endl;
co_yield i;
}
}
void call_GetSeq()
{
auto gen = GetSequenceGenerator(10, 5);
for (const auto& value : gen) {
std::cout << value << "(Press enter for next value)" << std::endl;
std::cin.ignore();
}
}
}
//main.cpp
void co_vs_callback();
void co_vs_return();
namespace hey_coroutines
{
void call_GetSeq();
void CallTest();
}
int main()
{
hey_coroutines::CallTest();
//hey_coroutines::call_GetSeq();
//co_vs_callback();
//co_vs_return();
return 0;
}
上面的文件共需要三个测试的cpp和一个主文件cpp,运行结果很是有意思,这里只看其一个:
call coroutine----1
Add100ByCallback: 10
call coroutine----1 yyyy:110
Add100ByCallback: 110
call coroutine----1 xxxxx:210
get result from coroutine1: 210
程序在回调结果和直接调用的结果上都保持了一致,这样看来,异步编程以后可能就会变得简单易用。
通过上述的代码可以看到,未封装的代码和封装的代码还是有一些不同的,这里分析一下c++20中未封装的代码,实现协程需要处理以下步骤:
1、首先要处理关键字:co_wait,co_return,co_yield,可能这些关键字还会在新标准中不断的完善。
2、要实现awaitable,其实就是实现await_ready、await_suspend、await_resume。
3、要实现自己的promise,operator new,operator delete,get_return_object,initial_suspend,final_suspend,unhandled_exception,return_void或者return_value
4、实现相关上层逻辑
5、调用
上面的promise_type和Awaitable大家可能会觉得陌生,其实在标准的文档中都有解释,比对代码和名称,一目了然。如果有兴趣可以看看:
https://lewissbaker.github.io/
上面有相当清楚的分析。
五、总结
协程因为不需要在像线程一样不断的上下文切换,所以可以认为无限制的创建,一般来说,线程创建过百,普通的PC和服务器基本就体会不到线程的优势了,但协程却可以创建几万几十万,甚至更多。网上有资料说甚至可以创建亿级以上,这个暂时还不能实战来验证。
不过协程的看似同步化操作,仍然是大幅的降低了异步编程的门槛,以后如果封装得体,会在未来产生不可估量的影响,c++的编程难度在底层复杂性增加的前提下,应用开始大幅降低难度,这对c++语言本身应该是一个非常利好的消息。