GMP模型

2022/7/3 23:22:58

本文主要是介绍GMP模型,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Golang协程调度器原理 & GMP设计思想

地址:https://www.bilibili.com/video/BV19r4y1w7Nx

1.Golang调度器的由来

1.1 早期单进程操作系统

所有进程只能顺序执行,产生问题:

  • 单一执行流程,计算机只能一个任务一个任务进行
  • 进程阻塞导致CPU浪费 (即某个进程阻塞,会导致后面程序无法继续执行)

1.2 多进程/多线程操作系统

​ 以时间片轮询的机制并发执行程序 (并行与CPU核数有关),很显然多线程/多进程可以解决1.1中进程阻塞导致CPU浪费的问题(即是你程序阻塞,时间片到了,也会强制释放CPU)。产生问题:

  • CPU高消耗

    ​ 以时间片轮询机制而言,为了保存原有线程的系统调用或者相关资源环境等,必然涉及到拷贝复制的过程,就会涉及到切换成本,造成CPU浪费。因此线程越多,切换过程就会更加频繁,所以线程并不是越多越好(线程越多抢占CPU会越有利)。

  • 内存高占用

    在32bit操作系统中,一个进程占用虚拟内存4GB;而一个线程占用4MB左右;因此多进程/多线程会导致内存高占用的问题。(而协程占用KB级别)

1.3 协程

​ 为了解决1.2的CPU高消耗和内存高占用问题,而内核态无法修改,所以尝试修改用户态。将线程分为用户线程和内核线程。而内核线程称为线程,用户线程称为协程,由于CPU视野只有内存空间,因此协程的开辟对CPU来说是无感的

​ 线程通过协程调度器绑定多个协程,而CPU视野只有内核空间,所以对CPU而言只有单一线程即进程,因此此方法可有效解决CPU高消耗的问题

​ 每个语言对协程进行不同处理。Golang对协程进行相应优化:对协程co-routine重命名为goroutine;修改协程内存大小,每个goroutine只有几KB大小,因此可以大量创建;可灵活调度,切换成本较低。所以最后重点就落到了优化协程调度器上面。

2.协程调度器和GMP模型设计思想

2.1 早期协程调度器

​ 各个线程首先需要去全局G队列拿锁,才能去执行协程挂载的任务,此时该线程不释放锁就导致其他线程无法去执行协程上的任务。

缺点:

  • 创建,销毁,调度协程都需要先去获取锁,这就导致形成了激烈的锁竞争
  • CPU在线程之间频繁切换会增加系统开销;

2.2 GMP模型简介

G ------ goroutine协程
P ------ 协程调度器
M ------ 线程

​ 每个 P 保存了当前执行的协程G内部资源信息(堆栈地址和变量参数等),所以M要先去获取P才能去执行G。创建的G会优先存放在本地队列,如果本地队列满了(最多256个G),会存放至全局G队列。

P 的个数,可由环境变量中$GOMAXPROCS设置;或在程序中可通过runtime.GOMAXPROCS()设置。

2.3 调度器设计策略

2.3.1 复用线程

复用线程可避免创建与销毁线程中进行的资源消耗;

实现的两种机制:

  • work stealing机制

    ​ 当线程M1P绑定,正在执行协程G1,而此时线程M2空闲,此时M2的协程调度器P会从M1的本地协程队列中偷取协程G到自己这边执行。

  • hand off机制

    ​ 当此时M1M2正常执行协程所挂载的任务,突然协程G1发生阻塞现象(比如read/write/channel阻塞等),这时系统会尝试唤醒/创建一个线程M3(优先唤醒,符合复用线程的思想),并把与当前阻塞线程M1绑定的协程调度器P转移到新的线程M3上,并把原来阻塞线程M1所占用的CPU进行释放。后续执行完成后,如果G1需要执行会被重新加入队列进行执行,M1会被睡眠或者销毁。

2.3.2 利用并行

​ 可充分发挥多核优势,通过设置GOMAXPROCS设置协程调度器的个数,通常并不会挂满,设置为CPU核数/2。

2.3.3 抢占

​ 相较于老的调度器而言,老调度器中只有当当前协程释放CPU,另一个协程才去执行;现在调度器以时间片而言,一个时间片到了后会强制释放CPU给其他协程使用。

2.3.4 全局G队列

​ 当线程空闲时,会首先从其他线程对应协程本地队列偷取(即work stealing机制),如果偷不到,会从全局G队列进行获取(前提要先去获取锁)。

2.4 "go func()"的历程

开始:

  • 1.执行go func() 会创建一个协程G
  • 2.创建的G优先会被调度到创建G线程对应的本地队列,如果本地队列已满,则G会被加入到全局队列;
  • 3.线程M会通过协程调度器P获取协程G执行。执行go func()之前,如果本地队列为空,优先会从其他线程对应的本地队列偷取G执行,即是work sealing机制;若其他线程对应的本地队列为空,则会从全局队列获取G进行执行;
  • 4.以时间片循环执行go func()对应的执行代码;即上图中(4-调度,5-执行,6-时间片返回,时间片到了会重新加入到本地队列)

如执行go func()代码产生阻塞现象:

为了节省资源,提高CPU利用率

  • 5.系统首先会从本地休眠线程队列中唤醒一个线程M来接管当前正阻塞的线程对应的P和本地G队列;如果本地休眠线程队列没有,则会新创建一个线程M,即hand off 机制;阻塞协程G会和当前线程M进行绑定;

  • 6.当阻塞完成后,线程M会被加入到休眠线程队列或者被销毁掉,而G则会被加入到其他本地队列,如果本地队列都满了,则会被加入到全局队列;

2.5 调度器的生命周期

`

2.6 可视化的GMP编程

地址:https://www.bilibili.com/video/BV19r4y1w7Nx?p=7&spm_id_from=pageDriver



这篇关于GMP模型的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程