原文
C++11里也能玩无栈协程了?
答案是:可以!
事实上异网在很早时,C++11里就可用无栈协程写异步代码了,只不过用起来不太方便,来看看C++11里怎么用异网无栈协程写一个回音服务器的吧.
#包含 <异网.h++>
#包含 <内存>
#包含 <向量>
#包含 <异网/产生.h++>
用 异网::异步写;
用 异网::缓冲;
用 异网::ip::传控;
用 异网::错误码;
用 大小型;
构 会话:异网::协程
{共针<传控::套接字>套接字_;共针<向量<符>>缓冲_;会话(共针<传控::套接字> 套接字): 套接字_(套接字),缓冲_(新 向量<符>(1024)){}空 符号()(错误码 ec = 错误码(), 大小型 n = 0){如 (!ec) 再入 (本){对 (;;){产生 套接字_->异步读些(缓冲(*缓冲_), *本); 产生 异步写(*套接字_, 缓冲(*缓冲_, n), *本); }}}
};
构 服务器 : 异网::协程
{异网::io服务& io服务_;共针<传控::受者> 受者_;共针<传控::套接字> 套接字_;服务器(异网::io服务& io服务): io服务_(io服务),受者_(新 传控::受者(io服务, 传控::端点(传控::v4(), 54321))){}空 符号()(错误码 ec = 错误码()){再入 (本){对 (;;){套接字_.重置(新 传控::套接字(io服务_));产生 受者_->异步接受(*套接字_, *本);io服务_.提交(会话(套接字_));}}}
};
#包含 <异网/坚定.h++>
整 主()
{异网::io服务 io服务;io服务.提交(服务器(io服务));io服务.跑();
}
先看回声会话部分的代码:
空 符号()(错误码 ec = 错误码(), 大小型 n = 0){如 (!ec) 再入 (本){对 (;;){产生 套接字_->异步读些(缓冲(*缓冲_), *本); 产生 异步写(*套接字_, 缓冲(*缓冲_, n), *本); }}}
//其中
对 (;;)
{产生 套接字_->异步读些(缓冲(*缓冲_), *本); 产生 异步写(*套接字_, 缓冲(*缓冲_, n), *本);
}
和C++20的协程代码是不是很像:
对 (;;)
{协待 套接字_->异步读些(缓冲(*缓冲_), 异网::用可等待); 协待 异步写(*套接字_, 缓冲(*缓冲_, n), 异网::用可等待);
}
所以C++11里面也是可通过协程来写异步代码的.
但是异网的无栈协程实现比较简陋,使用起来有不少约束.首先要把用无栈协程的文件用两个头文件包装起来,因为内部是宏实现的,编译完之后需要解定义.
#包含 <异网/产生.h++>
#包含 <异网/坚定.h++>
第二个不便之处是不自由,使用协程的对象要从异网::协程继承,然后定义符号,注意该符号是怎么写的:
空 符号()(错误码 ec = 错误码(), 大小型 n = 0){如 (!ec) 再入 (本){对 (;;){产生 套接字_->异步读些(缓冲(*缓冲_), *本); 产生 异步写(*套接字_, 缓冲(*缓冲_, n), *本); }}}
相信你第一次看到该代码时会很懵,这是个啥意思,再入是什么鬼,看不懂该代码.不要着急,听我来给你解惑该符号.
参数是错误码和传输大小,最开始时是默认的,为何这两个参数呢?注意函数中的再入(本),它表明当前该函数是可重入的,则何时会重入呢?
每次产生返回时就重入了,产生之后ec和大小就是新的了.比如产生 套接字_->异步读些(缓冲(*缓冲_),*本);
返回后,就可得到错误码和读到的大小了,所以完整写法应该是:
空 符号()(错误码 ec = 错误码(), 大小型 n = 0){如 (!ec) 再入 (本){对 (;;){产生 套接字_->异步读些(缓冲(*缓冲_), *本);//协程重入如(ec){输出<< "读错误消息: " << ec.消息() << "\n";断;}输出 << "读大小: " << n << "\n";产生 异步写(*套接字_, 缓冲(*缓冲_, n), *本); //协程重入如(ec){输出<< "写错误消息: " << ec.消息() << "\n";断;}输出<<"写大小:"<<n<<"\n";}}}
现在看懂了,该怪异写法了吧.
接着,如果想开启新协程要怎么做,看服务器接受的代码:
空 符号()(错误码 ec = 错误码()){再入 (本){对 (;;){套接字_.重置(新 传控::套接字(io服务_));产生 受者_->异步接受(*套接字_, *本);io服务_.提交(会话(套接字_));}}}
在产生受者_->异步接受(*套接字_,*本);之后通过提交,把会话对象丢到io环境线程池中了,后续会执行该会话对象,此时,你可能有疑问,为啥可把对象丢到线程池里?
答案很简单,因为会话有符号它就成为一个函数对象了.
为什么这里要把会话函数对象丢到线程池里呢,为啥不继续产生呢?因为这里想立即再次接受而不是等待会话结束,如果产生 会话那就没办法继续接受了.
通过提交非阻塞方式调用会话的协程,两个协程不会相互阻塞.
除此外,异网还提供了另外一个创建子协程的方法:
空 服务器::符号()(异网::错误码 ec, 大小型 长度)
{如 (!ec){再入 (本){干{套接字_.重置(新 传控::套接字(受者_->取执行器()));产生 受者_->异步接受(*套接字_, *本);分叉 服务器(*本)();//`分叉`之后父子协程都会往下执行到此} 当 (是父());产生 套接字_->异步读些(缓冲(*缓冲_), *本); 产生 异步写(*套接字_, 缓冲(*缓冲_, n), *本); }}
}
异网可通过分叉来创建子协程,分叉之后父协程和子协程都会往下执行,那如何区分父子协程呢?通过是父()来区分.
父协程是接受协程,当走到是父()时,它会继续接受,而子协程则会跳出当循环,往下去读和写了.
这就是通过分叉创建新协程的方法.
总结
异网里面有很多有趣的东西,以前还没注意C++11里用可无栈协程写异步代码,也是其余远调用维护时想用协程去改造,然后发现了该东西,它已有很久的历史了,但我现在才注意到,确实能简化编写异步代码,后面C++11版本的其余远调用会使用异网无栈协程去写.
不过写法比使用C++20协程库麻烦多了,但是考虑毕竟是C++11,能用协程已不错了,如果能升级到20标准还是用20协程舒服.