从后台工程师的角度说,有栈协程的应用更普遍。例如,云风封装的非常经典的基于C的ucontext.h来实现的共享栈的协程,具体请见《C 的 coroutine 库》。而golang在语言级实现的协程是独立栈的协程。
独立栈的协程实现相比共享栈的方式而言少了在每次切换上下文时候的栈数据拷贝,理论上来说性能更高一些,但是也有这样的问题:
之前一直对无栈协程关注不够,认真学一下后,做了如下总结,然后自己写一些代码来模拟无栈协程的运作方式:
假设有这样一个需求:服务器端的一段业务逻辑,需要同时访问A,B,C三个接口,如果串行访问,那么整个延迟就是ABC三条接口的延迟之和;在ABC三条接口相互不依赖的情况下,我们尝试用无栈协程的方式并发的访问三条接口。
通过gevent能够很好的把python的串行代码修改为并行代码。
from gevent import monkey; monkey.patch_all()
import gevent
import urllib2
def f(url):
print('GET: %s' % url)
resp = urllib2.urlopen(url)
data = resp.read() #每次做IO的时候都会导致上下文切换,这里相当于调用非阻塞的read(),然后yield
print('%d bytes received from %s.' % (len(data), url))
def main():
gevent.joinall([
gevent.spawn(f, 'https://www.python.org/'),
gevent.spawn(f, 'https://www.yahoo.com/'),
gevent.spawn(f, 'https://github.com/'),
])
if __name__=="__main__" :
main()
struct TcpClientContext{
int state; //记录代码执行到哪一行了
bool is_complete; //是否已经执行完成
//以下是业务相关的字段
const char* addr;
int fd;
char send_buffer[MAX_SEND_BUFFER_LEN];
char send_data_len;
int send_len;
char recv_buffer[MAX_RECV_BUFFER_LEN];
char body_len;
int recv_len;
char header_buffer[MAX_HEADER_LEN];
}
void main(){
struct TcpClientContext context1 = {.state=0, .is_complete=false, .addr="192.168.0.11:8080"};
struct TcpClientContext context1 = {.state=0, .is_complete=false, .addr="192.168.0.12:8080"};
struct TcpClientContext context1 = {.state=0, .is_complete=false, .addr="192.168.0.13:8080"};
do {
//以下的三个函数看起来会是并行执行的
http_fetch(&context1);
http_fetch(&context2);
http_fetch(&context3);
} while(context1.is_complete && context2.is_complete && context3.is_complete);
}
//模拟generator的伪代码
void http_fetch(struct TcpClientContext* ctx){
//函数的第一部分是要决定跳转到哪里
switch (ctx->state){
case 0: goto Label_0;
case 1: goto Label_1;
case 2: goto Label_2;
case 3: goto Label_3;
case 4: goto Label_4;
case 5: goto Label_5;
}
Label_0:
//连接服务器
ctx->fd = socket(/*伪代码*/);
setNonBlocking(ctx->fd);
connect(ctx->fd, ctx->addr);
//
ctx->state = 1; //进入下一个阶段
return;
Label_1:
//发送数据阶段
ctx->send_data_len = make_send_data();
ctx->send_len = 0;
do{
ctx->send_len += write(ctx->fd, ctx->send_buffer+ctx->send_len, ctx->send_data_len-ctx->send_len);
ctx->state = 2; //进入下一个阶段
return;
Label_2:
}while (ctx->send_len<ctx->send_data_len);
//接收数据阶段
ctx->recv_len = 0;
//先读固定字节的header
do {
ctx->recv_len += read(ctx->fd, ctx->header_buffer, MAX_HEADER_LEN);
ctx->state = 3; //进入下一个阶段
return;
Label_3:
} while(ctx->recv_len<MAX_HEADER_LEN);
ctx->body_len = ntohl(ctx->header_buffer);
ctx->recv_len = 0;
do {
ctx->recv_len += read(ctx->fd, ctx->read_buffer+ctx->recv_len, ctx->body_len - ctx->recv_len);
ctx->state = 4; //进入下一个阶段
return;
Label_4:
}while (ctx->recv_len<ctx->body_len);
ctx->is_complete = true;
close(ctx->fd);
ctx->fd = 0;
Label_5:
ctx->state = 5; //进入下一个阶段
return;
}
看了上面的代码,我们会想:太复杂了,谁会这么写代码?以上只是用C代码来模拟无栈协程的运行模式而已,实际上自带generator(生成器)能力的编程语言会用一些语法糖来屏蔽复杂的切换细节,可以参考python+gevent的实现。
Have Fun,希望你后续能够愉快的使用无栈协程。:-)