现在的位置: 首页 > 综合 > 正文

协程、线程和执行上下文

2018年02月03日 ⁄ 综合 ⁄ 共 1707字 ⁄ 字号 评论关闭

摘要: 本文介绍协程、线程及它们执行的上下文等概念,同时给出注意事项。协程是用户级的任务调度,线程是内核级的任务调度,而任务调度过程都涉及到上下文切换(保存与恢复),本文将从较为深刻的角度来阐述这些概念,及其相互关系。

协程和线程

线程在现代的系统里扮演的角色很重要,几乎一个现代一点的,稍微复杂大型一点的程序,往往都会引入线程,实现某种意义的并行。线程存在广泛的支持,从操作系统,到编译器,再到语言的定义与实现者,他们都在努力让线程更好用,也有更少的限制。相反协程的现实要惨淡很多,在操作系统那里很少有协程的影子;只有Windows里,存在一个协程的实现——fiber(在那里它被称为纤程)。

从概念上来说,线程和协程最大的区别就是调度方式不同——线程是被动调度的,协程是主动调度的。也就是说,线程什么时候执行,什么时候不执行,完全是OS决定,线程是OS的菜,程序员最多只能主动让线程停下来,却很难让线程主动运行起来。相反协程的执行与停止完全由程序员决定,何时执行、何时停止简单地调用一个协助方法就可以完成。因为线程是被动调度的,所以线程需要大量同步方法;而协程是主动调度的,所以协程之间的同步可以很自然完成——该停该跑——就这简单直接。

协程的概念很完美,使用可能也很方便。但是因为一些我还不了解的原因,在C/C++里并没有广泛使用协程库可用,这是因为现在的OS和编译器无法很好地支持协程的实现;这也许是历史原因,线程在概念上与线程不相矛盾,但它的许多实现方式限制了实现通用安全的协程。在C语言和UNIX草莽朝代,实现一个协程要比实现一个函数(协程的一种特例)更困难,而对于当时来说,函数已经完全够用了,如此协程被冷淡了。

在用户层协程被死死地限制了,可是在Linux内核里,它却一个基本的构件——内核线程,在更多的时候,更一个协程。当然这句话是我自己说,在我很有限的Linux内核经验上说的,极有可能是错误的。

执行上下文

线程是操作系统实现的,也是操作系统的调度单位。我们知道,所谓调度就是操作系统根据一些规则决定在何时中止一个线程操作,并在一个恰当的时候再唤醒这个线程。这里有个自然的问题,就是操作系统怎么实现中止与唤醒操作的?其时停止与唤醒操作都离不开CPU的支持,CPU在硬件上实现了中断机制,OS使用中断机制实现了线程调度。OS可以告诉CPU,每过100ms(毫秒)或10ms你就调用一下一段代码——这就是所谓的时钟中断例程,OS在这一小段代码里实现了线程的中止与唤醒。在这段代码里,OS通常会做这些事:

  • 保存一下当前的状态,当前是哪个线程在执行,当前这个线程执行到哪里了,等等——这些状态实际上就是当前CPU或CPU-Core里的数个特殊寄存器的数值。我们称这些状态可能放在一个结构体里,这个结构体又被称为上下文(执行上下文)。
  • 干一些其它的事,如更新系统时间,看看有没有什么其它OS需要定时去做的事,等等。
  • 根据一些规则(也就是调度策略),在所有已经中止的线程挑选一个,把之前保存起来的状态恢复——实际上就是把之前保存在结构体的寄存器的值,再加载到相应寄存器里。
  • 最后,OS执行一个跳转指令,把CPU的执行权限传递给刚才挑选的线程,如此这个线程就从之前停下来的地方接着跑起来了。也就是说,它被唤醒了。

在上面的说明,我们看到一次保存状态和一次恢复状态,所谓状态就是上下文(状态),而这个一次保存和一次恢复的过程称为一次上下文切换——大概意思就是说,刚才CPU在干这块的活,现在它又切换到另一块地方干活了。

现在我们知道上下文切换,以及OS是如何实现上下文切换的了,那么有一个问题,就是程序员能不能在自己的程序里使用与OS相同功能的代码,在自己的程序进行上下文切换?答案是: 完全可以。事实上,这就是实现协程的基本方法。程序在执行的时候是没有函数这些概念的,诸如函数这些概念都上层语言及编译器实现的抽象概念,CPU不认识。CPU只认识指令及指令操作的地址。

了解 C++ Boost 库的人一定知道它最近新加了两个库: context 和 coroutine —— 看这个名字就知道,前者是实现了上下文切换的,后者是利用前者实现了用户态协程的。

抱歉!评论已关闭.