Go语⾔“调度器”的由来

早期的单进程操作系统,⾯临两个问题。
(1)单⼀的执⾏流程。计算机只能⼀个任务⼀个任务处理,所有的程序⼏乎是阻塞的,更不⽤
说具备图形化界⾯或者⿏标这种异步交互的处理能⼒。
(2)进程阻塞所带来的CPU时间浪费。在⼀个进程完整的⽣命周期中,所要访问的物理部分包
括CPU、Cache、主内存、磁盘、⽹络等,不同的硬件媒介处理计算的能⼒相差甚⼤。如果将这
些处理速度不同的处理媒介通过⼀个进程串在⼀起,则会出现⾼速度媒介等待和浪费的现象。如
当⼀个程序加载⼀个磁盘数据的时候,在读写的过程中,CPU处于等待状态,那么对于单进程的
操作系统来讲,很明显会造成CPU运算能⼒的浪费,因为CPU此刻本应该被合理地分配到其他进
程上去做⾼层的计算。
那么能不能有多个进程来宏观地⼀起执⾏多个任务呢?后来操作系统就具有了最早的并发能
⼒,即多进程并发。当⼀个进程阻塞的时候,切换到另外等待执⾏的进程,这样就能尽ᰁ把CPU
利⽤起来,CPU也就不那么浪费了。
多进程/多线程的操作系统解决了阻塞的问题,⼀个进程阻塞CPU可以⽴刻切换到其他进程 中去执⾏,⽽且调度CPU的算法可以保证在运⾏的进程都可以被分配到CPU的运⾏时间⽚。从宏
观来看,似乎多个进程是在同时被运⾏
时间⽚(Timeslice)⼜称为“ᰁ⼦(Quantum)”或“处理器⽚(Processor Slice)”,是分时
操作系统分配给每个正在运⾏的进程微观上的⼀段CPU时间(在抢占内核中是从进程开始运⾏直
到被抢占的时间)。现代操作系统(如:Windows、Linux、Mac OS X等)允许同时运⾏多个进
程——例如,可以在打开⾳乐播放器听⾳乐的同时⽤浏览器浏览⽹⻚并下载⽂件。事实上,虽然
⼀台计算机通常可能有多个CPU,但是同⼀个CPU永远不可能真正地同时运⾏多个任务。在只考
虑⼀个CPU的情况下,这些进程“看起来像”同时运⾏的,实则是轮番穿插地运⾏,由于时间⽚通
常很短(在Linux上为5~800ms),⽤户不会感觉到。
时间⽚由操作系统内核的调度程序分配给每个进程。⾸先,内核会给每个进程分配相等的初
始时间⽚,然后每个进程轮番地执⾏相应的时间,当所有进程都处于时间⽚耗尽的状态时,内核
会᯿新为每个进程计算并分配时间⽚,如此往复。
但新的问题⼜出现了,进程拥有太多的资源,进程的创建、切换、销毁都会占⽤很⻓的时
间,CPU虽然利⽤起来了,但如果进程过多,CPU会有很⼤的⼀部分被⽤来进⾏进程切换调度
对于Linux操作系统来⾔,CPU对进程和线程的态度是⼀样的,如图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关系和 M N 关系

你可能感兴趣的:(深入理解Go,Go)