关于为什么突然就想学习一下Coroutine
,其实历程有点曲折。一开始是在看APUE
的时候看到了setjmp
和longjmp
两个神奇的东西。然后查了一下,发现竟然可以用来实现协程。。。
setjmp
和longjmp
下面是一段longjmp
和setjmp
的示例代码。该程序做的是不停地每隔一秒(不准确)打印一次”你好”。该程序的执行流程是这样,第一次先调用setjmp
,此时setjmp
返回结果为0,与此同时,根据man
,setjmp
把栈指针、PC
及一些其他寄存器的值保存在jmp_buf
中。然后if
语句为假,直接执行下面的longjmp
。由于调用longjmp
时候,传入了1,且longjmp
会利用jmp_buf
里面保存的信息,跳回setjmp
处。此时根据longjmp
传入的1,setjmp
返回1,则进入if
语句块,执行printf
。由于setjmp
又一次保存了信息,那么下一次longjmp
又可以跳回。因此循环往复,程序将不停地打印”你好”。
协程
协程,按字面意思,应该是相互协作的程序。在执行过程中,协程可以主动放弃CPU
,让另外一个协程运行,以此实现协同工作。咋一看,协程和线程也差不多嘛,都是一个先执行一下,再切换到另一个执行,循环往复,直至执行结束。其实还是有区别的。多线程可以跑在多个核上,并由OS
进行调度。而协程是非抢占式的,由用户态进行调度,由协程来决定何时交出CPU
,多个协程共用CPU
,实际上可以看成是单线程的性能。因此在不同的任务情景下,协程和线程可以体现出它们间的性能差异。
CPU
密集型
需要大量计算的程序,可以看出,像非抢占式的协程,很有可能导致时间片分配不均匀的问题。而且单核上跑,性能显然不能和多线程在多核上跑比。
IO
密集型
CPU
速度不知道比IO
速度高到哪里去了。因此,对于IO
密集型程序,即使你多线程,由于瓶颈在IO
,CPU
的优势也没法发挥出来,反倒是如果协程一遇到IO
,就主动交出CPU
,这样由于协程较线程更为轻量,不仅切换开销低,在相同并发度下,负载也相对更低。不仅如此,由于协程相比多线程而言,还不需要加锁。此时看起来协程更优。
协程示例
下面这个Python
程序模拟了基于协程的生产者消费者问题。首先是producer.send(None)
启动generator
。然后consumer
通过send
切换到producer
,让producer
生产,然后producer
通过yield
切换回consumer
,让consumer
消费。这样就可以协作以实现生产消费。
|
|
回到setjmp
和longjmp
看下面一个程序
该程序看起来和上面的那个没啥区别,实际上由于setjmp
保存状态和longjmp
恢复状态不在同一个作用域,会出现奇怪的问题,像crash
退出后,其实它调用setjmp
保存的那一个状态已经不能用了。虽然此处没有出现错误,但是调试可以发现像数组c
的值在longjmp
回去后已经变为了”\0”串,打印时是没有输出的。这样一来,如果协程用这个来实现貌似很不靠谱,连栈帧状态都不能保存,怎么保证协程切换之后原来的上下文有效呢?查了一下其实是有ucontext.h
这种东西的。这是一个*nix
系统才有的东西,可以用来保存上下文。这样看来就比较完美了。但是还有一个问题,怎么对付IO
处理的那些函数呢?像下面这个函数,就真正提高了性能吗?
|
|
即使用了协程,也yield
主动放弃了CPU
,但是,如果send
之类的I/O
函数没有实现非阻塞I/O
,这有意义吗?所以一个可能的实现,把send
之流搞成非阻塞的,调完之后就把CPU
让出去,然后当网络事件到达就再考虑切回来。这样看来,似乎可以简化程序的编写。如果直接上非阻塞的I/O
多路复用而不是协程,想必要实现多阶段功能,就要通过传送的包里面的状态码之类的东西进行状态标识。而用协程来搞的好处就在于,直接不用管,一个函数的逻辑就从头写到尾。遇到I/O
就及时切出CPU
,让主调度线程来负责调度。下面说到的Go
用的就是类似这种方案。
Goroutine
关于Go
语言,一直以来的印象就只有那个萌萌的吉祥物和高并发、易开发,直到前几天第一次接触Go
入门教程。。。由于不懂golang
,查了一下发现有一个人总结的很好。golang的goroutine是如何实现的?(Yi Wang翻译的答案)
可以看出,Goroutine
对应内核线程为M:N
的关系。多个Goroutine
由Scheduler
调度,被放到内核线程上跑,由于保存了上下文,就可以由Scheduler
来实现切换等操作。然后Go
又把I/O
封装成非阻塞,底层是用的I/O
多路复用,写的时候就不用管了,可以当成是同步的写法,调I/O
函数时候注册事件,让出CPU
,由Scheduler
把其它协程切入,当epoll
/kqueue
监听到I/O
事件到达,Scheduler
就可以调度,把CPU
还给该进程。相当于是不需要手动写epoll
代码,而是直接通过go/channel
就可以实现提高性能的目的,简化了编程。
参考资料
- golang的goroutine是如何实现的?(Yi Wang翻译的答案)
- The Go netpoller
- APUE(第3版 中文翻译版)
- 《学习GO语言》中文版 邢星译