一、线程模型

  • N:1模型,N个用户空间线程在1个内核空间线程上运行。优势是上下文切换非常快但是无法利用多核系统的优点。
  • 1:1模型,1个内核空间线程运行一个用户空间线程。这种充分利用了多核系统的优势但是上下文切换非常慢,因为每一次调度都会在用户态和内核态之间切换。(POSIX线程模型(pthread),Java)
  • M:N模型, 每个用户线程对应多个内核空间线程,同时也可以一个内核空间线程对应多个用户空间线程。Go打算采用这种模型,使用任意个内核模型管理任意个goroutine。这样结合了以上两种模型的优点,但缺点就是调度的复杂性。

下面看看golang的协程调度

  • M:一个用户空间线程,同时对应一个内核线程,类似posix pthread
  • P:代表运行的上下文环境, 也就是我们上一节实现的调度器,一个调度器也会对应一个就绪队列
  • G:goroutine,即协程

二、调度模型简介

groutine能拥有强大的并发实现是通过GPM调度模型实现,下面就来解释下goroutine的调度模型。

Go的调度器内部有三个重要的结构:M,P,G
M:M是对内核级线程的封装,数量对应真实的CPU数,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息
G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
P:P全称是Processor,处理器,它的主要用途就是用来执行goroutine的。每个Processor对象都拥有一个LRQ(Local Run Queue),未分配的Goroutine对象保存在GRQ(Global Run Queue )中,等待分配给某一个P的LRQ中,每个LRQ里面包含若干个用户创建的Goroutine对象。

Golang采用的是多线程模型,更详细的说他是一个两级线程模型,但它对系统线程(内核级线程)进行了封装,暴露了一个轻量级的协程goroutine(用户级线程)供用户使用,而用户级线程到内核级线程的调度由golang的runtime负责,调度逻辑对外透明。goroutine的优势在于上下文切换在完全用户态进行,无需像线程一样频繁在用户态与内核态之间切换,节约了资源消耗。

调度实现

从上图中看,有2个物理线程M,每一个M都拥有一个处理器P,每一个也都有一个正在运行的goroutine。
P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。
图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),
Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个
goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。

当一个OS线程M0陷入阻塞时(如下图),P转而在运行M1,图中的M1可能是正被创建,或者从线程缓存中取出。

当MO返回时,它必须尝试取得一个P来运行goroutine,一般情况下,它会从其他的OS线程那里拿一个P过来,
如果没有拿到的话,它就把goroutine放在一个global
runqueue里,然后自己睡眠(放入线程缓存里)。所有的P也会周期性的检查global
runqueue并运行其中的goroutine,否则global runqueue上的goroutine永远无法执行。
 
另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global
runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run
queue的一半,这就确保了每个OS线程都能充分的使用,如下图:

三、GPM创建相关问题

M和P的数量如何确定?或者说何时会创建M和P?

1、P的数量:

  • 由启动时环境变量$GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定(默认是1)。这意味着在程序执行的任意时刻都只有$GOMAXPROCS个goroutine在同时运行。

2、M的数量:

  • go语言本身的限制:go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略。
  • runtime/debug中的SetMaxThreads函数,设置M的最大数量
  • 一个M阻塞了,会创建新的M。

M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。

3、P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。

4、M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。

M选择哪一个P关联?

  • M会选择导致此M被创建的那个P关联。

什么时候会切换P与M的关联关系?

当M因系统调用而阻塞时(M上运行的G进入了系统调用的时候),M与P会分开,如果此时P的就绪队列中还有任务,
P就会去关联一个空闲的M,或者创建一个M进行关联。(也就是说go不是像libtask一样处理IO阻塞的?不确定。)

就绪的G如何选择进入哪个P的就绪队列?

  • 默认情况下:因为P的默认数量是1(M不一定是1),所以如果我们不改变GOMAXPROCS,无论我们在程序中用go语句创建多少个goroutine,它们都只会被塞入同一个P的就绪队列中。
  • 有多个P的情况下:如果修改了GOMAXPROCS或者调用了runtime.GOMAXPROCS,运行时系统会把所有的G均匀的分布在各个P的就绪队列中。

如何保证每个P的就绪队列中都会有G

如果一个P的就绪队列所有任务都执行完了,那么P会尝试从其他P的就绪队列中取出一部分到自己的就绪队列中,以保证每个P的就绪队列都有任务可以执行。

Golang 协程调度的更多相关文章

  1. 图解Go协程调度原理,小白都能理解

    阅读本文仅需五分钟,golang协程调度原理,小白也能看懂,超实用. 什么是协程 对于进程.线程,都是有内核进行调度,有CPU时间片的概念,进行抢占式调度.协程,又称微线程,纤程.英文名Corouti ...

  2. golang协程同步的几种方法

    目录 golang协程同步的几种方法 协程概念简要理解 为什么要做同步 协程的几种同步方法 Mutex channel WaitGroup golang协程同步的几种方法 本文简要介绍下go中协程的几 ...

  3. Python与Golang协程异同

    背景知识 这里先给出一些常用的知识点简要说明,以便理解后面的文章内容. 进程的定义: 进程,是计算机中已运行程序的实体.程序本身只是指令.数据及其组织形式的描述,进程才是程序的真正运行实例. 线程的定 ...

  4. 面试必问:Golang高阶-Golang协程实现原理

    引言 实现并发编程有进程,线程,IO多路复用的方式.(并发和并行我们这里不区分,如果CPU是多核的,可能在多个核同时进行,我们叫并行,如果是单核,需要排队切换,我们叫并发) 进程和线程的区别 进程是计 ...

  5. GO GMP协程调度实现原理 5w字长文史上最全

    1 Runtime简介 Go语言是互联网时代的C,因为其语法简洁易学,对高并发拥有语言级别的亲和性.而且不同于虚拟机的方案.Go通过在编译时嵌入平台相关的系统指令可直接编译为对应平台的机器码,同时嵌入 ...

  6. skynet源码阅读<5>--协程调度模型

    注:为方便理解,本文贴出的代码部分经过了缩减或展开,与实际skynet代码可能会有所出入.    作为一个skynet actor,在启动脚本被加载的过程中,总是要调用skynet.start和sky ...

  7. Golang协程实现流量统计系统(3)

    进程.线程.协程 - 进程:太重 - 线程:上下文切换开销太大 - 协程:轻量级的线程,简洁的并发模式 Golang协程:goroutine Hello world package main impo ...

  8. Openresty Lua协程调度机制

    写在前面 OpenResty(后面简称:OR)是一个基于Nginx和Lua的高性能Web平台,它内部集成大量的Lua API以及第三方模块,可以利用它快速搭建支持高并发.极具动态性和扩展性的Web应用 ...

  9. go协程调度

    目录 前言 1. 线程池的缺陷 2.Goroutine 调度器 3.调度策略 3.1 队列轮转 3.2 系统调用 3.3 工作量窃取 4.GOMAXPROCS设置对性能的影响 参考 前言 Gorout ...

随机推荐

  1. jquery判断是否是空对象 不含任何属性

    code function isEmptyObject(e) { var t; for (t in e) return !1; return !0 }

  2. Linux驱动之输入子系统简析

    输入子系统由驱动层.输入子系统核心.事件处理层三部分组成.一个输入事件,如鼠标移动.键盘按下等通过Driver->Inputcore->Event handler->userspac ...

  3. BZOJ1026或洛谷2657 [SCOI2009]windy数

    BZOJ原题链接 洛谷原题链接 简单的数位\(DP\),套模板就好. #include<cstdio> #include<cstring> using namespace st ...

  4. 19. pt-query-digest

    慢查询参数 slow_query_log=1slow_query_log_file=/mysql3306/log/slow.log 记录的是查询语句,而非管理语句.除非启用 los_slow_admi ...

  5. java多线程系列14 设计模式 Master-Worker

    Master-Worker模式是常用的并行设计模式,可以将大任务划分为小任务,是一种分而治之的设计理念. 系统由两个角色组成,Master和Worker,Master负责接收和分配任务,Worker负 ...

  6. AX_Dialog

    Dialog                      dialog  = new Dialog("@SYS1052");  DialogField                 ...

  7. async/await的特殊的地方

    一:async如果是用于方法声明里,那么要求这个方法的返回值必须是Task.Task<TResult>.void这三种,而且await出现的地方要求其所在的方法必须是async修饰的方法: ...

  8. 51nod1683

    代码参考:http://blog.csdn.net/sdfzyhx/article/details/74359927 //dp[i][j][k]表示到了第i列,最下边的点最短路为j,剩下的点之间的关系 ...

  9. SWPU-ACM集训队周赛之组队赛(3-11) E题题解

    点这里去做题 %*c  读入时跳过一位,本题中即跳过"-"; #include<stdio.h> int run(int x) //判断闰年 { ; ==&&a ...

  10. 2.DI依赖注入

    一:DI Dependency Injection ,依赖注入 is a :是一个,继承. has a:有一个,成员变量,依赖. class B { private A a;   //B类依赖A类 } ...