Go中的调度器(1)--背景
Contents
我们说逻辑控制流是并发的说的是他们在时间上重叠。并发不仅仅局限于操作系统内核,也可以应用于应用程序。
现代操作系统提供了三种基本方法来构建应用级并发(application-level concurrency):
- 进程(Processes)。每个逻辑控制流是一个进程,由kernel进行调度和维护。每个进程有独立的虚拟地址空间,想要和其他流通信,需要进程间通信(interprocess communication, IPC)机制。
- I/O多路复用。这种并发形式是在单个的进程上下文中需要显式的调度逻辑控制流。逻辑流被转化成状态机,当数据到达文件描述符后,main程序显式的从一个状态转换到另一个状态。因为是在单一的进程中,所有的逻辑流共享同一个地址空间。
- 线程(Threads)。线程同样是运行在单个进程的上下文中。不过其调度由kernel控制。可以讲线程看成是上面两种形式的混合。由kenel调度,但是共享同一个虚拟地址空间。
1. 进程的优劣
对于在父、子进程间共享信息有个清晰的模型:共享文件表,但是不共享用户的地址空间。这样一来,一个进程进程不会覆盖另一个进程的虚拟地址空间。
另一方面,独立的地址空间使得进程共享状态信息困难。它必须使用显式的IPC机制。并且进程控制和IPC的开销很高。
2. I/O多路复用的优劣
相对基于进程的设计,给了程序员更多对程序行为的控制能力,同时每个逻辑流都能访问该进程的全部地址空间。使得流之间共享数据变的容易。通常来说事件驱动设计常常比基于进程的设计要高效得多,因为它们不需要进程上下文切换来调度新的流。
另一方面,事件驱动的缺点是编码复杂。需要为每个并发粒度编写相应的代码。还有一个确定是它们不能充分利用多核处理器。
3. 基于线程的并发编程
同一个进程中允许同时运行多个线程。线程由kernel自动调度。每个线程都有它自己的thread context,其中包括唯一的Thread ID(TID)、栈、栈指针、程序计数器,通用目的寄存器和条件码。
基于线程的逻辑流,结合了进程和I/O复用的优点。线程由kernel自动调度,kernel通过ID来识别线程。多个线程共享虚拟地址空间的所有内容,包括代码,数据,堆,共享库,打开的文件。
3.1 线程执行模型
每个进程开始时都是单一线程,这个线程称为主线程(main thread)。在某时刻,主线程会创建对等线程(peer thread)。从这个时刻开始两个线程并发的运行。终于,因为主线程执行一个慢系统调用例如read,sleep或者被硬件的间隔计时器中断。。控制会通过上下文切换传递到对等线程,对等线程会执行一段时间,然后控制传递回主线程,依次类推。
线程的上下文回比一个进程的上下文小的多,线程的上下文切换要比进程的上下文切换块的多。和一个进程相关的线程构成了一个对等线程池,独立于其他进程创建的线程。主线程和其他对等线程的区别仅在于它总是进程中第一个运行的线程。
一个线程可以kill任何对等线程,或者wait它的对等线程终止。每个对等线程都能读写相同的共享数据。
虽然线程比较轻量,但是在调度时也有比较大的额外开销。每个线程会都占用 2MB的内存空间,在对线程进行切换时不止会消耗较多的内存,恢复寄存器中的内容还需要向操作系统申请或者销毁对应的资源。
协程(co-routine)
进程和线程都是由kernel调度的,上下文切换时仍有很大开销,并且这个操作局部性很差,需要大量的内存访问,增加了cpu访问内存的运行周期。我们需要一个更加轻量的线程,用户态线程。
用户态线程需要绑定内核态线程,cpu无法感知用户态线程。用户态线程通常叫做协程(co-routine),为了区分会将线程称作内核态线程。
协程的上下文切换完全在用户态进行,无需线程一样频繁在用户态和内核态之间切换。
Author againest1
LastMod 2020-03-08