您的位置:首页 > 其它

基于Libtask进行协程浅析

2016-12-07 22:34 344 查看

协程介绍

与子例程一样,协程也是一种程序组件。 相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。 协程源自Simula和Modula-2语言,但也有其他语言支持。 协程更适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道。 –维基百科

下面我们会以Libtask(Go语言的作者之一Russ Cox的作品)作为分析案例来解释协程的原理。

协程工作原理

要了解协程的工作原理,可以从以下几点入手:

1、上下文切换。

2、函数调用原理。

3、Libtask保存寄存器值的结构体。

4、Libtask的接口函数和实现。


1、上下文切换

当一个程序被执行(称为进程)的时候,这些寄存器的值通常会被修改。所以当要切换进程执行的时候,只需要把这些寄存器的值保存下来,然后把新进程寄存器的值赋值到CPU中(我们知道CPU的使命就是执行程序中的指令,而且CPU内部有很多用于存放数据的寄存器,其中比较重要的一个寄存器叫EIP寄存器,它用于存储下一条要执行的指令。除了EIP寄存器之外,还有一个比较重要的寄存器叫ESP寄存器,它用于保存程序的栈顶位置。除此之外,CPU还有很多其他用途的寄存器,如:通用寄存器EAX、EDX和段寄存器CS、DS等等。),那么就完成进程切换了,通常我们把这个过程称为上下文切换,协程的切换也类似。

2、函数的调用原理

以C语言为例,函数调用时通过栈结构来保存现场和恢复现场的。比如,在第189行有这样一段代码来进行函数调用:
demoFunc(a, b, c, d);
那么,第190行代码的地址会被放在栈底,然后,实参从右往左一次入栈。这样一来,当该函数完毕,程序又会恢复到之前调用处(189行)的下一行(190行)。原理如下图(图片来自百度):


3、Libtask保存寄存器值的结构体

前面说过,要进行上下文切换,存储对应寄存器的值是必不可少的。Libtask通过引进
struct mcontext
这个结构体来保存对应的寄存器的值,以下是
struct mcontext
这个结构体的源码实现,命名比较规范,感兴趣的读者,可以通过名称查询对应寄存器的功能。

struct mcontext {
int mc_gs;
int mc_fs;
int mc_es;
int mc_ds;
int mc_edi;
int mc_esi;
int mc_ebp;
int mc_isp;
int mc_ebx;
int mc_edx;
int mc_ecx;
int mc_eax;
int mc_trapno;
int mc_err;
int mc_eip;
int mc_cs;
int mc_eflags;
int mc_esp;
int mc_ss;
};


4、Libtask的接口函数和实现。

参考上文提到的函数调用的切换,Libtask提供了三个接口来实现协程的切换。

1)通过
int getcontext(struct mcontext *ctx);
获取当前上下文(也就是将对应寄存器的值,存入
struct mcontext
结构对应的变量中):

gexcontext:
movl    4(%esp), %eax
movl    %fs, 8(%eax)
movl    %es, 12(%eax)
movl    %ds, 16(%eax)
movl    %ss, 76(%eax)
movl    %edi, 20(%eax)
movl    %esi, 24(%eax)
movl    %ebp, 28(%eax)
movl    %ebx, 36(%eax)
movl    %edx, 40(%eax)
movl    %ecx, 44(%eax)
movl    $1, 48(%eax)
movl    (%esp), %ecx
movl    %ecx, 60(%eax)
leal    4(%esp), %ecx
movl    %ecx, 72(%eax)
movl    44(%eax), %ecx
movl    $0, %eax
ret


2)通过
int setcontext(struct mcontext *ctx);
来设置上下文(也就是将
struct mcontext
结构对应变量的值赋给对应寄存器):

setcontext:
movl    4(%esp), %eax
movl    8(%eax), %fs
movl    12(%eax), %es
movl    16(%eax), %ds
movl    76(%eax), %ss
movl    20(%eax), %edi
movl    24(%eax), %esi
movl    28(%eax), %ebp
movl    36(%eax), %ebx
movl    40(%eax), %edx
movl    44(%eax), %ecx
movl    72(%eax), %esp
pushl   60(%eax)
movl    48(%eax), %eax
ret


3)通过
int swapcontext(struct mcontext *new, struct mcontext *old)
来进行上下文的切换,这个实现是基于上文提到的两个函数,实现比较简单:

int swapcontext(struct mcontext *new, struct mcontext *old)
{
getcontext(old);
setcontext(new);
return 0;
}


个人总结

为什么协程会被引入

对于一些场景,如远端数据库服务器的调用,早期的服务器端编程,使用消息机制来进行事务的处理。由于过于占用CPU资源,因此,引进了事件机制,如epoll等。又由于会产生无谓的阻塞浪费线程资源,因此,引进了协程来解决这个问题。

多协程和多线程目的比较

多线程是为了压榨CPU的能力,多协程是为了压榨每个线程(每个进程默认有一个主线程)的能力。

协程的使用

1、适用于该线程没有被充分利用的场景,如果该线程已经很忙了,则没有必要引入协程了。

2、由于多协程是使用单线程模拟出来的,因此,不能使用阻塞调用,因为,这样会卡住该线程(卡住所有协程)。

3、由于多协程是使用单线程模拟出来的,所以,对于临界资源的访问不用加锁。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: