基于Libtask进行协程浅析

来源:互联网 发布:经传软件智能辅助线 编辑:程序博客网 时间:2024/06/15 06:12

协程介绍

与子例程一样,协程也是一种程序组件。 相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。 协程源自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、由于多协程是使用单线程模拟出来的,所以,对于临界资源的访问不用加锁。

0 0