现在无论是客户端、服务端或web开发都会涉及到多线程的概念。那么大家也知道,线程是操作系统能够进行运算调度的最小单位,同一个进程中的多个线程都共享这个进程的全部系统资源。

线程

三个基本概念

  • 内核线程:在内核空间实现的线程,由内核管理
  • 用户线程:在用户空间实现的线程,不归内核管理,由用户态完成管理
  • 轻量级进程(LWP):在内核中支持用户线程(用户线程与内核线程的中间层,内核线程的高度抽象)

线程模型

  1. 一对一模型(内核级线程模型)

    由进程创建LWP,由于每个LWP对应一个内核级线程,所以用户也就是在创建一个一个内核线程,由内核管理线程。我们熟知的大多数语言就是属于一对一模型,比如C#、java、c等(这些语言也有相应的方式实现用户态线程,只是语言本身的线程是一对一模型)

  2. 一对多模型(用户级模型)

    用户进程创建多个用户级线程对应在同一个LWP上,所以本质上在内核态只会有一个内核线程运行,那么用户及线程的调度就是在用户态通过用户空间的线程库对线程进行管理。

    这类模型优点就是调度在用户态,所以不用频繁地进行内核态与用户态切换,大大提升线程效率。python语言的协程就是这种模型实现的,但是这种模型并不能实现真正意义上的的并发。

  3. 多对多模型(用户级与内核级线程模型)

    多对多模型也就是在上述两个模型基础上做一些优点的集合。用户态的多个线程对应多个LWP,但它们之间不是一一对应的,在用户态管理调度每个LWP对应的用户线程绑定关系。

    所以多对多模型是可以有更多的用户态线程在相对于比较少的内核线程上运行的。这种用户态线程也就是我们平时说的协程。Go语言的协程就是多对多模型,它由Go语言的runtime在用户态做调度,这也是Go语言高并发的原因。

协程(Goroutine)

前面我们说到,协程就是用户态线程,它的所有调度都在用户态实现,协程这方面Go语言应该是最具代表性的(毕竟以高并发为卖点),以Go语言的协程--Goroutine作为研究对象。

GM模型

在Go语言设计的初期,Go团队使用简单的GM模型实现协程。

G:goroutine,也就是协程,它一般所占的栈内存为2KB,运行过程中如果栈空间不够用则会自动扩容。它可以被理解成一个被打包的代码段。goroutine并不是一个执行单元。

M:machine工作线程,就是真正用来执行代码的线程。由Go的runtime管理好它的生命周期。

在Go语言初期使用一个全局的队列来保存所有的G。当用户新建一个G的时候,runtime就会将打包好的G放到全局队列的队尾,并维护好队尾指针。当M执行完一个G后,就会到全局队列的队头取一个G给M执行,并且维护好全局队列的队头指针。

可以发现这里有个很严重的问题,如果放任随意加入G,也放任任何M随意取G,那么就会出现并发问题。解决并发问题很简单,就是加锁,Go语言团队也确实是这样做的。那么加了锁之后也就代表所有的存取操作都会涉及到这个单一的全局互斥锁,那整个调度的执行效率大打折扣。除了锁导致性能问题以外,还有类似系统调用时,工作线程经常被阻塞和取消阻塞,等等一些问题2。所以后续官方团队调整了调度模型,加入了P的概念。

GMP模型

新的模型中引入了P的概念,G与M没有什么大的变化。

P:process处理器,代表了M所需的上下文环境,也可以理解为一个M运行所需要的Token,当P有任务时需要创建或者唤醒一个M来执行它队列里的任务。所以P的数量决定了并行执行任务的数量,可以通过runtime.GOMAXPROCS来设定,现在的Go版本中默认为CPU的核心数。

上图根据曹大的GMP模型图自行简化绘制的。

P结构:一个P对应一个M,P结构中带有runnext字段和一个本地队列,runnext字段中存储的就是下一个被M执行的G,每个P带有一个256大小的本地数组队列,这个队列是无锁的,这两个数据结构类似于缓存的概念,可以有效的解决全局队列被频繁访问的问题,而且runnext和本地队列是PM结构本地的,没有其他的执行单元竞争,所以也不用加锁。

M结构

runtime中有两个特殊的M:

①是主线程,它专门用来处理主线逻辑;

②是一个监控线程sysmon,它不需要P就可以执行,监控线程里面是一个死循环,不断地检测是否有阻塞或者执行时间过长的G,发现之后将抢占这些G。

普通的M:

普通的M会有很多个,空闲的M会被放在全局调度器的m idle(m 空闲)链表中,在需要m的时候会优先从这里取,取不到的话就会创建新的M,M创建后就不会销毁了。每个M结构有三个G,gsignal是M专门处理runtime的信号的G,可以处理一些唤醒机制,G0是在执行runtime调度代码的时候需要切换到的G,还有一个G就是当前M需要执行的用户逻辑的G。

M有几种状态:

  • 自旋中(spinning): 暂时还没找到 Goroutine 来执行,但是正在找的一个状态。有这个状态和 nmsping 的计数,能够帮 runtime 判断是不是需要再启动额外的线程来执行 goroutine;
  • 执行go代码中: M正在执行go代码, 这时候M会拥有一个P;
  • 执行原生代码中: M正在执行原生代码或者阻塞的syscall, 这时M并不拥有P;
  • 休眠中: M发现无待运行的G时会进入休眠,并添加到空闲M链表中, 这时M并不拥有P。

调度

整体的调度流程就是一个生产消费过程。用户作为生产者,用户起了一个goroutine后,被打包成一个G,经过runtime的一系列调度后被M消费。

生产端

生产过程主要是指将一个打包好的G放入到队列中的过程。

消费端

消费过程相较于生产过程复杂非常多,主要描述P和M怎样被调度,最终由M完成执行的过程。

前面我们说到,每个M中有一个特殊的G0用来做调度,可以理解成当我们的M没有执行完一个G后,需要切换到G0的栈空间,开始调用schedule函数找一个G来执行,找到后就切到找到的G的栈空间,并且执行它。

这边注意到,在每次执行的时候,会有一个变量在不断的++,这个schedtick我们会在每次schedule函数调度G的时候用到,接下来我们就来看看,每次调用schedule函数是如何找G的。

可以从图中看到,整个寻找可执行G的过程,这个过程中只要找到G,就会结束schedule,返回拿到的G去执行。

  1. 在schedule函数中会判断 schedtick%61 == 0,也就是每61次(为什么是61次?我也不知道,算是个魔法数字,凭作者自己的考虑)就去全局队列取一次(确保全局队列中的G会被执行到)。否则我们就去当前M绑定的P的队列中拿,当然优先执行runnext中的G。

  2. 当本地队列中找不到可用的G的时候我们就会进入一个findrunnable的函数专门用来找G。

  3. 进入findrunnable中首先也是先在当前绑定的P的runnext和本地队列中寻找。

  4. 从全局队列中获取,获取的数量是 全局队列的长度/gomaxprocs+1 ,但是最大只能拿128个G到本地队列中,并拿出一个执行。

  5. 从netpoll网络轮询器(监控网络I/O和文件I/O等耗时操作的)中获取阻塞的G继续执行。

  6. work stealing,去其他P(随机算法随机选中的)的本地队列中窃取G到自己这边,窃取的数量是被窃取队列的一半。

  7. 再次尝试去全局队列获取。

  8. 检查所有空闲的P队列,如果有可运行的G,就去绑定到那个P并执行。

  9. 再次尝试去netpoll中获取。

    最后,在整个findrunnable中都没有找到可以执行的G,当前的M就会进入休眠,并放到全局M链表中等待唤醒。

本文中,忽略了GC方面处理以及很多细节处理,真正的调度流程是非常复杂的,Go语言的官方代码是开源的并且有详细的注释,并且在Go版本升级过程中也会对一些逻辑有所调整,请自行注意时效性(本文基于Go1.15)

References

图解协程调度模型-GMP模型的更多相关文章

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

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

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

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

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

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

  4. Python开发——15.协程与I/O模型

    一.协程(Coroutine) 1.知识背景 协程又称微线程,是一种用户态的轻量级线程.子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完 ...

  5. Python进阶(5)_进程与线程之协程、I/O模型

    三.协程 3.1协程概念 协程:又称微线程,纤程.英文名Coroutine.一句话说明什么是线程:协程是一种用户态的轻量级线程. 协程拥有自己的寄存器上下文和栈.协程调度切换时,将寄存器上下文和栈保存 ...

  6. Golang 协程调度

    一.线程模型 N:1模型,N个用户空间线程在1个内核空间线程上运行.优势是上下文切换非常快但是无法利用多核系统的优点. 1:1模型,1个内核空间线程运行一个用户空间线程.这种充分利用了多核系统的优势但 ...

  7. Openresty Lua协程调度机制

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

  8. Kotlin协程解析系列(上):协程调度与挂起

    vivo 互联网客户端团队- Ruan Wen 本文是Kotlin协程解析系列文章的开篇,主要介绍Kotlin协程的创建.协程调度与协程挂起相关的内容 一.协程引入 Kotlin 中引入 Corout ...

  9. go协程调度

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

随机推荐

  1. 联想INTEL X86台式机 用光驱启动 usb光驱启动

    联想INTEL X86台式机  用光驱启动 usb光驱启动 启动项顺序 都要调整 主要顺序 自动顺序 出错顺序 按下f10 f12

  2. ELK学习实验019:ELK使用redis缓存

    1 安装一个redis服务 [root@node4 ~]# yum -y install redis 直接启动 [root@node4 ~]# systemctl restart redis [roo ...

  3. JFlash ARM对stm32程序的读取和烧录-(转载)

    本篇文章主要是记录一下JFlash ARM 的相关使用和操作步骤,读取程序说不上破解,这只是在没有任何加密情况下对Flash的读写罢了!在我们装了JLINK驱动后再根目录下找到JFlash ARM , ...

  4. python3 smtplib发送邮件

    使用smtp包发送邮件还依赖email的一些方法 发送邮件主要分为三步: 1,定义邮箱参数:邮箱服务器地址,邮箱用户名,邮箱密码,邮件发送方,邮件接收方,邮件标题,邮件内容 2,配置发送内容 3,实例 ...

  5. ace-editor

    1. ace-editor编辑器只读状态:editor.setReadOnly(true);  // false to make it editable 2. ace-editor设置程序语言模式:e ...

  6. 开源项目核心商城(CoreShop)

    帮小伙伴推一下他的开源项目作者是@大灰灰 核心商城(CoreShop)Beta 支持可视化布局的.Net小程序商城 [![star](https://gitee.com/CoreUnion/CoreS ...

  7. THINKPHP_(8)_修改TP源码,支持基于多层关联的任一字段进行排序

    之前博文 前述博文THINKPHP_(1)_修改TP源码,支持对中文字符串按拼音进行排序,其解决的主要问题是,对于查询出的think\collection数据,按指定字段对数据进行排序,从而在页面上进 ...

  8. Git 快速控制

    Git 快速控制 聊聊学习 Git 那些事 现在回想起来,其实接触 Git 的时候是在大一的时候表哥带入门的.当时因为需要做一个项目,所以他教如何使用 Git 将写好的代码推送到 GitHub 上,然 ...

  9. 多实例gpu_MIG技术快速提高AI生产率

    多实例gpu_MIG技术快速提高AI生产率 Ride the Fast Lane to AI Productivity with Multi-Instance GPUs 一.平台介绍 NVIDIA安培 ...

  10. 基于TensorRT车辆实时推理优化

    基于TensorRT车辆实时推理优化 Optimizing NVIDIA TensorRT Conversion for Real-time Inference on Autonomous Vehic ...