操作系统(6)-协程

一、什么是协程?

【百度百科】协程与子例程一样,协程(coroutine)也是一种程序组件。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。

协程更适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道。 协程的概念1963年就被提出来了。但直到最近几年才在某些语言(如Lua)中得到广泛应用。现在在面试的时候,有可能会被问到。笔者就有这样的经历。

协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用

一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。

二、各大语言对协程的支持程度

1.Lua从5.0版开始支持协程的概念,极大的扩展了Lua的能力。

Lua的协程通过扩展库coroutine来实现,其中的所有函数如下(具体可以参考Lua的官方manual):

coroutine.create
coroutine.resume
coroutine.running
coroutine.status
coroutine.wrap
coroutine.yield

当前运行的代码可以看作运行在主协程中(就像C程序的main运行在主线程中),通过create可以创建一个协程,resume以运行此协程,直到新协程调用yield程序才能返回到”主协程“中运行。

协程通常是纯软件实现的多任务,与CPU和操作系统通常没有关系,所以没有理论上限。唯一的缺点似乎就是:它不能同时将 CPU 的多个核用上。但对 lua 来说这通常不是问题,因为一个宿主程序里面是可以允许有多个 lua 状态机的,开多个线程或进程,然后每个核开一个 lua 状态机即可。

2.C++通过Boost.Coroutine实现对协程的支持

3.Java不支持

4.Python通过yield关键字实现协程,Python3.5开始使用async def对原生协程的支持

5.Go语言的Goroutine

Goroutine 其实就是协程解决方案的一种演进和实现。

  • 首先,它内置了 Coroutine 机制。因为要用户态的调度,必须有可以让代码片段可以暂停/继续的机制。
  • 其次,它内置了一个调度器,实现了 Coroutine 的多线程并行调度,同时通过对网络等库的封装,对用户屏蔽了调度细节。
  • 最后,提供了 Channel 机制,用于 Goroutine 之间通信,实现 CSP 并发模型(Communicating Sequential Processes)。因为 Go 的 Channel 是通过语法关键词提供的,对用户屏蔽了许多细节。其实 Go 的 Channel 和 Java 中的 SynchronousQueue 是一样的机制,如果有 buffer 其实就是 ArrayBlockQueue。

三、协程的优缺点

优点:

  • 协程更加轻量,创建成本更小,降低了内存消耗
    协程本身可以做在用户态,每个协程的体积比线程要小得多,因此一个进程可以容纳数量相当可观的协程
  • 协作式的用户态调度器,减少了 CPU 上下文切换的开销,提高了 CPU 缓存命中率
    协作式调度相比抢占式调度的优势在于上下文切换开销更少、更容易把缓存跑热。和多线程比,线程数量越多,协程的性能优势就越明显。进程 / 线程的切换需要在内核完成,而协程不需要,协程通过用户态栈实现,更加轻量,速度更快。在重 I/O 的程序里有很大的优势。比如爬虫里,开几百个线程会明显拖慢速度,但是开协程不会。

但协程也放弃了原生线程的优先级概念,如果存在一个较长时间的计算任务,由于内核调度器总是优先 IO 任务,使之尽快得到响应,就将影响到 IO 任务的响应延时。假设这个线程中有一个协程是 CPU 密集型的他没有 IO 操作,也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况,所以这种情况下需要程序员自己避免。

此外,单线程的协程方案并不能从根本上避免阻塞,比如文件操作、内存缺页,这都属于影响到延时的因素。

  • 减少同步加锁,整体上提高了性能
    协程方案基于事件循环方案,减少了同步加锁的频率。但若存在竞争,并不能保证临界区,因此该上锁的地方仍需要加上协程锁。
  • 可以按照同步思维写异步代码,即用同步的逻辑,写由协程调度的回调
    需要注意的是,协程的确可以减少 callback 的使用但是不能完全替换 callback。基于事件驱动的编程里面反而不能发挥协程的作用而用 callback 更适合。

缺点:

  • 在协程执行中不能有阻塞操作,否则整个线程被阻塞(协程是语言级别的,线程,进程属于操作系统级别)
  • 需要特别关注全局变量、对象引用的使用
  • 协程可以处理 IO 密集型程序的效率问题,但是处理 CPU 密集型不是它的长处。
    假设这个线程中有一个协程是 CPU 密集型的他没有 IO 操作,也就是自己不会主动触发调度器调度的过程,那么就会出现其他协程得不到执行的情况,所以这种情况下需要程序员自己避免

四、协程跟多线程的区别

协程的执行有点像多线程,但协程的特点在于是一个线程执行,那和多线程比,协程有何优势?

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

五、协程的适用场景

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

Coroutines非常适合于实现熟悉的程序组件,例如协作任务、异常、事件循环、迭代器、无限列表和管道。

协程的出现,改变了函数只有唯一运行入口的观点,一个较长的过程,尤其是过程中存在需要等待的步骤,也可以用一个函数来编写,等待的时候“跑”到函数外干点别的,然后再回来接着原来的步骤继续运行,如果采用回调的方式,需将这个过程切成一块一块的,块与块之间可以做点别的事情。

协程的出现使我们的编程方式多了一种选择,但协程的编程,尤其是与一些I/O异步操作结合起来用时,难度还是比较大的。

六、总结

Coroutine(协程)是一种用户态的轻量级线程,特点如下:

1、 轻量级线程;
2、 非抢占式多任务处理,由协程主动交出控制权;
3、 编译器/解释器/虚拟机层面的任务;
4、 多个协程可能在一个或多个线程上运行;

借用Donald Knuth的一句话总结协程的特点:“子程序就是协程的一种特例。”


参考文章

1、 https://baike.baidu.com/item/%E5%8D%8F%E7%A8%8B/8652240?fr=aladdin
2、 https://www.liaoxuefeng.com/wiki/897692888725344/923057403198272
3、 https://www.jianshu.com/p/49e80a3d4e04
4、 https://blog.csdn.net/weixin_42825585/article/details/87891968
5、 https://blog.csdn.net/guxch/article/details/82803769
6、 https://www.jianshu.com/p/2782f8c49b2a

版权声明:本文不是「本站」原创文章,版权归原作者所有 | 原文地址: