Go语言进阶三——GMP原理与调度

225 阅读5分钟

我正在参加「掘金·启航计划」

golang“调度器”的由来

1、单进程时代不需要调度器

一切的软件都跑在操作系统上,真正干活的是cpu,早期的操作系统每个程序就是一个进程,直到一个程序运行完,才能进行下一个进程,就是“单进程时代”,一切的程序只能串行发生。

早期的单进程操作系统,面临两个问题:

1.单一的执行流程,计算机只能一个任务一个任务的处理

2.进程阻塞所带来得CPU时间浪费

2、多进程/多线程时代有了调度的需求

在多进程/多线程操作系统中,解决了阻塞的问题,一个进程阻塞cpu可以立马切换到其他进程中去执行,但是问题又出现了,进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,cpu有很大一部分都被用来进程调度了。

3、协程来提高CPU利用率

多进程、多线程已经提高了系统的并发能力,但是为每个任务都创建一个线程是不现实的,因为会消耗大量的内存。

大量的进程/线程出现了新的问题:

  • 高内存占用
  • 调度的高消耗CPU

\

其实一个线程分为“内核态”线程和“用户态”线程。一个用户态线程必须绑定一个内核态线程,但是cpu并不知道有用户态线程的存在,他只知道它运行的是一个内核态线程。

这样,我们再去细化去分类一下,内核线程依然叫线程(thread),用户线程叫协程(co-routine)。

4、协程与线程的映射关系

N:1关系

N个协程绑定1个线程,优点是协程在用户态线程就可以完成切换,不会陷入内核态

缺点:

  • 一个进程的所有协程都绑定在一个线程上
  • 某个程序用不了硬件的多核加速能力
  • 一旦某个协程阻塞了,造成线程阻塞,本进程的其他协程都无法执行了,本本就没有并发的能力了。

1:1关系

一个协程绑定一个线程,容易实现,协程调度都由cpu完成,不存在N:1缺点

缺点:

  • 协程的创建、删除和切换都由CPU完成,有点略显昂贵

M:N关系

M个协程绑定一个线程,是N:1和1:1类型的结合,克服了以上两个模型的缺点,但实现起来比较复杂

协程跟线程是有区别的,线程由cpu调度是抢占式的,协程由用户态调度是协作式的,一个协程让出cpu后,才执行下一个协程。

5、Go语言的协程goroutine

Go为了提供更容易使用的并发方法,使用了goroutinechannelgoroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime调度,转移到其他可运行的线程上,关键是,程序员看不到这些底层细节,降低了编程的难度,提供了更容易的并发。

Go中,协程被称为goroutine,它非常轻量,一个goroutine只占几kb,并且这几kb就足够goroutine运行完,这就能在有限的内存内支持大量goroutine,支持了更多的并发。虽然一个goroutine的栈只占几kb,但实际是可伸缩的,如果需要更多内容,runtime会自动为goroutine分配。

Goroutine特点:

  • 占用内存小
  • 调度更灵活(runtime调度)

6、被抛弃的goroutine调度器

大部分文章都会用G来表示Goroutine,用M来表示线程。

下面我们来看看被废弃的golang调度器是如何实现的?

M想要执行、放回G都必须访问全局G队列,并且M有多个,即多线程访问同一资源需要加锁进行保证互斥/同步,所以全局G队列是有互斥锁进行保护的。

缺点:

  • 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。
  • M转移G会造成延迟和额外的系统负载,比如当G中包含创建新协程的时候。

Goroutine调度的GMP模型的设计思想

在新调度器中,除了M(thread)和G(goroutine),又引入了P(Processor)。

Processor,它包含了运行goroutine的资源,如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列。

GMP模型

在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上。

  • 全局队列:存放等待运行的G。
  • P的本地队列:同全局对列类似,存放的也是等待运行的G,存放的数量有限,不超过256个,新建G’时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
  • P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个。
  • M:线程想运行任务旧的获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。