感谢同事的分享,对协程有了初步的认识。
一.基本概念
协程(Coroutine, 又被称呼为“纤程”、“微线程”)顾名思义就是“协作的例程”(Co-operative routines)。跟具有操作系统概念的线程不一样,协程是在用户空间利用程序语言的语法语义就能实现逻辑上类似多任务的编程技巧。
简单的说,协程就是类似函数一样的程序组件,程序员可以在一个线程里面轻松创建数十万个协程,就像数十万次函数调用一样。然而,不同的是,函数只有调用入口一个起始点,返回之后函数就结束了;协程的入口可以是起始点,也可以接着从上一个返回点继续执行,即可重入。协程与协程直接可以平级的转移执行权(yield),而普通函数则必须遵守调用与被调用的非平级关系。
二.协程示例
典型的消费者-生产者模式中,传统的解决方案是多线程+互斥锁,即一个线程写数据,一个线程读数据,用互斥锁保护临界区,如下图所示。
缺点:1 互斥锁的问题:阻塞挂起,优先级反转,死锁。 2 创建线程开销昂贵。
如果改用协程,解决方案如下:生产者协程生产数据后,直接通过yield跳转到消费者协程开始执行,待消费者消费数据完毕后,切换回生产者继续生产数据。
伪代码如下:yield是指,当前协程让出执行权,使得其他协程获得执行权并运行。
producer_coroutine() {
loop
while queue is not full
create some new items
add the items to queue
yield to consumer_coroutine
}
consumer_coroutine(){
loop
while queue is not empty
remove some items from queue
use the items
yield to producer_coroutine
}
main(){
create producer_coroutine
create consumer_coroutine
resume producer_coroutine
}
协程的优点:
1. 代码可读性强,整个过程可以在一个线程内由用户态自行调度完成;
2. 无需内核调度子系统参与切换上下文。
协程的缺点:
相比多线程的办法,损失了使用多核的能力。
三.协程,进程,线程区别
进程:一个进程就是一个正在运行的程序实例,包括程序计数器,寄存器和变量当前值。每个进程都有它虚拟的cpu和虚拟内存地址空间,资源句柄等,不同进程彼此隔离。Linux内核中每个进程都使用一个名为task_struct的结构来描述。这个结构包含了进程的权限,pid,ppid, 子进程链表,进程组关系,调度优先级,运行状态,namespace, 安装的信号处理程序,虚拟内存等重要信息。创建进程的方法:fork()。
线程:进程并不是操作系统的唯一程序运行方式,另一种形式是线程。UNIX以及类UNIX系统中,线程是以轻量级进程的形式实现。在linux内核中,每个线程也拥有独立的task_struct结构,因此,每个线程也拥有自己独立的pid。一个进程中可以包含多个同时运行的线程,这些线程共享了同一个虚拟内存地址空间和系统资源。Linux下线程的创建:
1 直接使用系统调用:clone(),fork()也是调用clone()。
2 创建POSIX线程:pthread_create(),实际也是调用的clone()。
协程:与进程、线程最大的区别在于:协程并不是操作系统语义下的概念。操作系统并不参与协程的控制和调度,这一切完全在用户态进程内完成。切换协程并不需要陷入内核态,不需要操作系统的调度子系统参与,避免了进程线程间的上下文切换,无需更新进程虚拟内存页表和寄存器信息。相比之下,协程的调度切换可以完全在用户态进行,无需操作系统干涉,代价低廉。因此,无论是创建协程,还是切换协程,代价都远远小于创建进程/线程和切换进程/线程。
四.协程的实现
一些高级编程语言已经在语言层次上实现了协程,比如go, lua;另外一些语言通过第三方库实现了协程,比如python的gevent, nodejs的fiber。C/C++目前只能通过第三方库来实现协程,例如boost coroutine, libtask(go语言的启发者), libco(来自腾讯微信部门)。libco的源代码在github上可以找到。
协程实现的技术关键在于用户态程序员需要在进程内自行处理协程的调度与切换。C语言的库函数setjmp(3)和longjmp(3)提供了一种函数间跳转机制,其中setjmp(3)实现了保存当前函数的堆栈帧信息到jmp_buf结构中,longjmp(3)则负责通过jmp_buf保存的信息来修改esp和eip以及寄存器信息来实现跳转。goto只能在函数内跳转。
目前有部分协程库是基于setjmp(3)和longjmp(3)实现的, 而glibc对这两个函数的实现则直接采用了gcc内嵌汇编的方式。同时,也有部分协程库直接使用gcc内嵌汇编来完成协程间的上下文切换,主要是利用函数栈的方式实现。