深入理解Go语言
上QQ阅读APP看书,第一时间看更新

1.1.3 协程提高CPU的利用率

那么如何才能提高CPU的利用率呢?多进程、多线程已经提高了系统的并发能力,但是在当今互联网高并发场景下,为每个任务都创建一个线程是不现实的,因为这样就会出现极大量的线程同时运行,不仅切换频率高,也会消耗大量的内存:进程虚拟内存会占用4GB(32位操作系统),而线程也要大约4MB。大量的进程或线程出现了以下两个新的问题。

(1)高内存占用。

(2)调度的高消耗CPU。

工程师发现其实可以把一个线程分为“内核态”和“用户态”两种形态的线程。所谓用户态线程就是把内核态的线程在用户态实现了一遍而已,目的是更轻量化(更少的内存占用、更少的隔离、更快的调度)和更高的可控性(可以自己控制调度器)。用户态中的所有东西内核态都看得见,只是对于内核而言用户态线程只是一堆内存数据而已。

一个用户态线程必须绑定一个内核态线程,但是CPU并不知道有用户态线程的存在,它只知道它运行的是一个内核态线程(Linux的PCB进程控制块),如图1.4所示。

如果将线程再进行细化,内核线程依然叫线程(Thread),而用户线程则叫协程(Co-routine)。操作系统层面的线程就是所谓的内核态线程,用户态线程则多种多样,只要能满足在同一个内核线程上执行多个任务,例如Co-routine、Go的Goroutine、C#的Task等。

既然一个协程可以绑定一个线程,那么能不能多个协程绑定一个或者多个线程呢?接下来有3种协程和线程的映射关系,它们分别是N:1关系、1:1关系和MN关系。

图1.4 一个线程中的用户态和内核态

1.N:1关系

N个协程绑定1个线程,优点就是协程在用户态线程即完成切换,不会陷入内核态,这种切换非常轻量快速,但缺点也很明显,1个进程的所有协程都绑定在1个线程上,如图1.5所示。

图1.5 协程和线程的N:1关系

N:1关系面临的几个问题如下:

(1)某个程序用不了硬件的多核加速能力。

(2)某一个协程阻塞,会造成线程阻塞,本进程的其他协程都无法执行了,进而导致没有任何并发能力。

2.1:1关系

1个协程绑定1个线程,这种方式最容易实现。协程的调度都由CPU完成了,虽然不存在N:1的缺点,但是协程的创建、删除和切换的代价都由CPU完成,成本和代价略显昂贵。协程和线程的1:1关系如图1.6所示。

3.MN关系

M个协程绑定1个线程,是N:1和1:1类型的结合,克服了以上两种模型的缺点,但实现起来最为复杂,如图1.7所示。同一个调度器上挂载M个协程,调度器下游则是多个CPU核心资源。协程跟线程是有区别的,线程由CPU调度是抢占式的,协程由用户态调度是协作式的,一个协程让出CPU后,才执行下一个协程,所以针对MN模型的中间层的调度器设计就变得尤为重要,提高线程和协程的绑定关系和执行效率也变为不同语言在设计调度器时的优先目标。

图1.6 协程和线程的1:1关系

图1.7 协程和线程的MN关系