本文根据Golang深入理解GPM模型加之自己的理解整理而来

Go协程的引入及GMP模型

  • 一、协程的由来
    • 1. 单进程操作系统
    • 2. 多线程/多进程操作系统
    • 3. 引入协程
  • 二、golang对协程的处理
    • 1. 对co-routine对处理
    • 2. 对调度器的优化(早期)
  • 三、goroutine调度器的GMP模型
    • 1. gmp模型简介
    • 2. gmp模型调度器设计策略
      • 1. 复用线程
      • 2. 利用并行
      • 3. 抢占
      • 4. 全局g队列

一、协程的由来

1. 单进程操作系统

早期的单进程操作系统每个进程/线程(cpu无法区别线程还是进程)都是顺序执行的,

带来了两个问题:

  • 单一执行流程,计算机只能一个任务一个任务的进行处理
  • 进程阻塞带来的cpu时间浪费

2. 多线程/多进程操作系统

针对以上不足,引出了多线程/多进程操作系统

这种方式解决了多线程/多进程间的阻塞问题,但该方式又引入了新的问题,就是切换成本问题,从一个线程切换到下一个线程的时候需要保存当前线程的状态,会涉及各种系统调用、上下文切换、拷贝复制,这些东西我们称之为cpu浪费时间成本

这样会导致,如果进程/线程的数量过多的情况下,切换成本就更大,cpu利用率大大降低,一个cpu看起来可能满负载,但其实其中只有%50用来执行真正的程序,剩余时间都在进行频繁的线程切换。

不仅如此,多线程还往往伴随着同步竞争的问题,因此开发设计越来越复杂,在实际运行中,为了达到更好的并发效果,我们往往给单独的任务分配一个线程进行执行,当任务越来越多的情况下,不仅会造成cpu高消耗调度的问题,还会出现高内存占用的问题,在一个32的操作系统中,往往一个进程会占用4g的虚拟内存,一个线程会占用4m左右内存。

因此 提高cpu利用率、减小内存消耗 才是当今需要解决的事情,那么怎么优化呢?


3. 引入协程

一个线程分为内核态用户态两个部分,于是工程师们想着将这两部分分开,将一个线程分为用户线程、内核线程两部分,两者之间进行绑定,这样就可以各司其职,内核线程专门用来与硬件底层进行一些交互,而用户线程用来保证业务层面的并发效果,而且这样cpu的视野就只有内核线程,只与内核线程进行交互

将一个线程分开两部分以后,我们将其中的用户空间的部分称之为协程,将内核空间部分称之为线程

在以上基础上,可以进行进一步优化, 我们可以让一个(内核)线程通过一个协程调度器来绑定多个协程,这样cpu调度时无感,还是只针对(内核)线程,但是上层开了多个协程后,可以让每一个协程挂载一个任务,这样在用户态能够保证并发效果,且由于cpu只针对内核空间的线程,多个协程之间cpu是不需要切换的,这样就解决了前面高消耗cpu的瓶颈

也就形成了上图一个线程对应多个协程的关系,当今计算机都是多核的,因此线程和协程之间往往采用以下多对多的方式,每个cpu针对一个线程来绑定多个协程,这样可以达到更好的并发效果

由于内核空间的cpu对线程的调度无法干涉,因此优化的主要目标移动到了用户空间里对协程调度器的优化,如果对其进行优化能够达到更高的并发性。


二、golang对协程的处理

1. 对co-routine对处理

golang在对协程调度器处理之前,首先对co-routine协程进行了处理,首先将其改名字为goroutine,其次修改了goroutine的所占的内存大小,砍掉了多余不必要的空间,使得每个goroutin所占内存大小为几kb,实现了可以存在大量的goroutine,解决了高内存消耗的问题。


2. 对调度器的优化(早期)

然后便对协程调度器进行了优化,实现了灵活调度

golang早期调度器形式如下图所示,协程与线程之间是多对多的关系,其中维护了一个全局的goroutine队列,每创建一个协程,都将起放到这个队列中。当其中的线程要调度协程时,首先会获取全局队列的锁,然后尝试去执行goroutine,队列中剩余goroutine向队列头移动,当执行完后,将 执行完的goroutine放回队列尾部。

早期的调度器简单,但是存在很多的弊端:

  1. 创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。
  2. M转移G会造成延迟和额外的系统负载。
  3. 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。

三、goroutine调度器的GMP模型

1. gmp模型简介

  • G 指的是goroutine,协程,也就是上述一个线程分半后用户状态下的线程
  • P 指的是processor,用于处理执行goroutine,包含了每一个goroutine所需要的上下文环境。每个P维护了一个本地队列,存放当前P即将要执行的goroutine,此外还有一个全局队列,用于存放等待运行的goroutine。每个P的本地队列有数量限制,一般不超过256G,新建一个goroutine的时候,优先放到P的本地队列中,如果队列满了,才会尝试放到全局队列中。P的数量可以通过GOMAXPROCS()来设置,它代表了真正的并发度,即有多少个goroutine可以同时运行
  • M 指的是Machine,物理线程,也就是上述一个线程分半后内核状态下的线程

GMP模型的整个流程图如下所示,由全局队列、P的本地队列、P列表、M列表几个部分组成:

其中P列表是在程序启动时创建的,其数量最多有GOMAXPROCS个,有两种配置方式:

  • 通过配置环境变量$GOMAXPROCS
  • 在程序中通过runtime.GOMAXPROCS()方法来设置

M列表的数量表示当前操作系统分配给当前go程序的内核线程数,与P的数量无关,go语言本身限定了M的最大量为10000个,我们一般不对其数量进行设置,因为其数量是动态变化的,因为有一个M阻塞就会创建一个新的M,如果有M空闲,就会对其回收或者睡眠;如果需要对其数量进行设置,可以通过runtime/debug包下的SetMaxThreads函数进行设置

gmp模型的调度过程可以理解为,当一个任务需要执行时,首先由cpu调度分配一个线程M,然后M会获取该线程的P,P从本地队列/全局队列中取出一个G进行执行,也就是同一时间一个P只能执行一个G,因此一个程序当前所能执行最高的G的数量就是P的数量,也就是GOMAXPROCS

2. gmp模型调度器设计策略

gmp模型对调度器的优化主要集中在以下几个策略:

  1. 复用线程
  2. 利用并行
  3. 抢占
  4. 全局G队列

1. 复用线程

为了避免频繁的创建、销毁线程,而是对线程进行复用,gmp模型调度器采用了两种机制work stealinghand off

1️⃣ work stealing

假设M1与P1绑定正在执行G1协程,当前P1的本地队列中还有G2、G3等待被执行,但此时M2对应的P2空闲,work stealing机制就是M2想要执行协程的话就从M1的P1的本地队列中偷取G进行执行

2️⃣ hand off

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xTaE4Wus-1632563686593)(https://gitee.com/zhong_siru/images/raw/master//img/202109251753330.png)]

此时假设M1的P1正在执行G1,M2的P2即将执行G3,但此时G1阻塞,hand off机制就是将M1与其P1分离开,此时cpu会创建一个新的线程,然后将P1迁移到新创建的线程M3中,cpu调度执行M2和M3,相当于M3接管了M1之前绑定P继续执行。此时原来的M1和G1处与阻塞状态,如果G1的阻塞操作执行完毕后,还需要执行的话就会加入到其他队列中,如果不需要执行则直接被销毁。

2. 利用并行

该策略就是利用GOMAXPROCS来限定P的个数,例如设置为 CPU核数/2,这样该程序跑起来最多用到一半的cpu,其他的cpu给其他程序使用

3. 抢占

以前的co-routine绑定一个cpu的时候,如果此时来了其他的co-routine,只有当前co-routine结束主动释放时该cpu才会给其他co-routine进行绑定

而现在goroutine绑定一个cpu的时候,如果有其他的goroutine等待运行,则当前g最多执行10ms,10ms一到不管当前g是否主动释放,当前在等待的g一定会抢占cpu,这样保证了每个g都是平等的,防止饥饿现象

4. 全局g队列

如果一个P的本地队列里已经没有G待执行的话,会优先从其他P的本地队列里面偷,如果都没有的话才会从全局队列里面取,取出与放回的过程涉及全局队列的加锁与锁释放

通俗易懂的Go协程的引入及GMP模型简介相关推荐

  1. 从根上理解高性能、高并发(七):深入操作系统,一文读懂进程、线程、协程

    本文引用了"一文读懂什么是进程.线程.协程"一文的主要内容,感谢原作者的无私分享. 1.系列文章引言 1.1 文章目的 作为即时通讯技术的开发者来说,高性能.高并发相关的技术概念早 ...

  2. python 协程和异步的关系_python协程与异步协程

    在前面几个博客中我们一一对应解决了消费者消费的速度跟不上生产者,浪费我们大量的时间去等待的问题,在这里,针对业务逻辑比较耗时间的问题,我们还有除了多进程之外更优的解决方式,那就是协程和异步协程.在引入 ...

  3. 小议Python3的原生协程机制

    此文已由作者张耕源授权网易云社区发布. 欢迎访问网易云社区,了解更多网易技术产品运营经验. 在最近发布的 Python 3.5 版本中,官方正式引入了 async/await关键字.在 asyncio ...

  4. 协程概念,原理(c++和node.js实现)

    协程 什么是协程 wikipedia 的定义:协程是一个无优先级的子程序调度组件,允许子程序在特点的地方挂起恢复. 线程包含于进程,协程包含于线程.只要内存足够,一个线程中可以有任意多个协程,但某一时 ...

  5. 万字长文 | 漫谈libco协程设计及实现

    libco简介 libco是微信后台大规模使用的c/c++协程库,2013年至今稳定运行在微信后台的数万台机器上,使得微信后端服务能同时hold大量请求,被誉为微信服务器稳定性的基石.libco在20 ...

  6. python协成_Python协程技术的演进

    引言 1.1. 存储器山 存储器山是 Randal Bryant 在<深入理解计算机系统>一书中提出的概念. 基于成本.效率的考量,计算机存储器被设计成多级金字塔结构,塔顶是速度最快.成本 ...

  7. linux的进程/线程/协程系列5:协程的发展复兴与实现现状

    协程的发展复兴与实现现状 前言 本篇摘要: 1. 协同制的发展史 1.1 协同工作制的提出 1.2 自顶向下,无需协同 1.3 协同式思想的应用 2. 协程的复兴 2.1 高并发带来的问题 2.2 制 ...

  8. pdf 深入理解kotlin协程_协程初探

    Hello,各位朋友,小笨鸟我回来了! 近期学习了Kotlin协程相关的知识,感觉这块技术在项目中的可应用性很大,对项目的开发效率和维护成本有较大的提升.于是就考虑深入研究下相关概念和使用方式,并引入 ...

  9. 漫谈微信libco协程设计及实现(万字长文)

    欢迎关注作者git博客 1.libco简介   libco是微信后台大规模使用的c/c++协程库,2013年至今稳定运行在微信后台的数万台机器上,使得微信后端服务能同时hold大量请求,被誉为微信服务 ...

最新文章

  1. android模糊查询listview数据_ListView的简单应用(一)
  2. java web 默认页面配置文件_Tomcat中配置全局的错误页面(如404)+删除Tomcat中webapps目录下的自带项目,防止Tomcat默认文件泄露...
  3. Python中的split,rsplit,splitlines
  4. Android全局异常捕获
  5. 如何利用机器学习进行海量数据挖掘
  6. cs1.6的c语言源代码,cs1.6source - 源码下载|游戏|其他游戏|源代码 - 源码中国
  7. 学到了林海峰,武沛齐讲的Day16完
  8. linux查看gc日志,GC通用日志解读
  9. linux 网络速度非常慢,解决Ubuntu 10.04上网速度慢的问题
  10. 拓展名为html包括,在Windows中,帮助文件的扩展名为()。选项: a、“.html” b、“.sys” c、“.h...
  11. SAP 常用的库存表
  12. 今天我进了沼泽,总有一天我要走出来!
  13. 人生感悟|写在四月底
  14. 中国石油大学《化工原理二》第三阶段在线作业
  15. 什么是 Hash 算法?
  16. 【北邮国院大三上】互联网协议_Internet Protocol_PART A
  17. 全球IEEE期刊大全(综合整理,附原文论文下载地址)
  18. 2020中国农业银行 信息科技/金融科技岗 春招 笔试+面经
  19. sqlplus -prelim/ as sysdba用法
  20. 蓝桥杯-历届试题-猴子分苹果

热门文章

  1. kotlin重写构造方法编译报错:Primary constructor call expected
  2. 2022-2028年中国领带行业投资分析及前景预测报告
  3. 2022-2028中国橡胶衬里行业全景调研及竞争格局预测报告
  4. 2022-2028年中国聚乳酸降解塑料行业市场运营格局及投资前景趋势报告
  5. python内置库之学习ctypes库(二)
  6. PowerBuilder程序 ASA 数据库移植后不能连接解决
  7. Linux系统管理必备知识之利用ssh传输文件
  8. 各种注意力机制PyTorch实现
  9. 掩码语言模型(Masked Language Model)mlm
  10. pytorch JIT浅解析