Golang | GMP模型

2,188 阅读4分钟

进程&&线程&&协程

"三程":

  • 进程:程序的一次运行,资源分配(除CPU)的基本单位;
  • 线程:CPU调度的基本单位;
  • 协程:一种用户态、轻量级的线程;

用户态线程:

  • 内核看不到,不知道用户线程的存在,内核调度时,仍然以该进程为单位进行调度;
  • 用户级线程内核的切换由用户态程序自己控制内核切换(通过系统调用来获得内核提供的服务),不需要内核干涉,少了进出内核态的消耗,但不能很好的利用多核Cpu。
  • 任意时刻,每个进程只能够有一个用户线程在运行,尽管它是多线程的,这就不利于多核CPU的使用了;

协程就是一种用户态线程;

优点:线程的切换无需陷入内核,所以切换开销小,速度非常快;

缺点:同一个进程中同时只有一个线程在运行,一个线程的阻塞将导致整个进程的阻塞;

核心态线程:

  • 由内核进行管理,线程切换将会消耗较多资源。对于多核CPU,可进行多线程并行运行;
  • 内核能够感知到线程的存在,CPU调度时,是以线程为单位进行调度;

优点:可以多线程并行运行;

缺点:线程切换时,比较消耗资源。因为需要保存上下文,核心态和用户态的切换

两个切换的对比:

  • 用户线程切换:只设计基本的CPU上下文切换。切换是完全在用户态中进行的;
  • 内核线程的切换:也有上下文切换,且保存的东西更多一点。并且线程的调度只有内核才能完成,这就涉及到了用户态和核心态的切换,这才是最主要的开销;(进程的切换也要陷入内核)

内核线程和用户线程的联系

  • 一对一模型:充分利用了多核系统的优势但是上下文切换非常慢,因为每一次调度都会在用户态和内核态之间切换。POSIX线程模型(pthread)就是这么做的。
  • 多对一模型:多个用户空间线程在1个内核空间线程上运行。 优势是上下文切换非常快,因为只有一个内核线程嘛,用户线程切换时,内核的这个线程是不用动的。也就意味着没有了核心态和用户态的切换,以及内核线程的上下文保存。
  • 多对多模型:效率虽高,但是管理复杂;

Golang的Goroutine调度模型

goroutine就是协程,完全运行在用户态中,借鉴了N:M模型;

1、GMP模型

  • G:goroutine
  • M:Machine,内核线程
  • P:Logical Processor,处理器;代表了M所需要的上下文环境
    • runtime.GOMAXPROCS (numLogicalProcessors)可以设置多少个处理器,go 1.5开始,默认是CPU核数
    • 实际运行时P和CPU核心数并无任何关联,P最大不超过256;P可以理解为并行度的多少,也就是说当前最多只能有P个线程在运行;(是不是很像线程池)
    • P一旦初始化了,就不能修改了;

三者关系:

  • M的数量和P不一定匹配,可以设置很多M,M和P绑定后才可运行,多余的M处于休眠状态。
  • P包含一个LRQ(Local Run Queue)本地运行队列,这里面保存着P需要执行的协程G的队列。
  • 除了每个P自身保存的G的队列外,调度器还拥有一个全局的G队列GRQ(Global Run Queue),这个队列存储的是所有未分配的协程G。

2、go func()执行流程

  1. 创建一个G对象,加入到本地队列或全局队列;
  2. 如果还有空闲的P,则创建一个M;
  3. M会启动一个底层线程,并结合P,循环执行G;
  4. P执行G的顺序是,先从本地队列找,没有则到全局队列找(一次性转移[全局G个数/P个数]),再到其他P中找(一次性转移一半);
  5. G是执行顺序是按照队列顺序的;
  • P管理着G队列,但是G要运行,还需要M的绑定;
  • runtime.GOMAXPROCS只会影响P的数量,不会影响M的数量;
    • P和M的关系,就好比用户线程和内核线程的N:M模型
    • 没有足够的M关联P时,会创建M;在runtime执行系统监控或垃圾回收等任务的时候也会导致新的M的创建。 所以,runtime.GOMAXPROCS只是类似线程池的大小设置而已;
    • 当然,go也可以通过runtime/debug.SeMaxThreads限制操作系统线程数; SetMaxThreads主要用于限制程序无限制的创造线程导致的灾难。目的是让程序在干掉操作系统之前,先干掉它自己。
  • goroutine是按照抢占式调度的,一个goroutine最多执行10ms就会换作下一个;