目录

文章目录

  • 目录
  • GPM 调度模型
    • 基本概念
      • G(Goroutine)
      • P(Processor)
      • M(Machine)
      • Golang Runtime Scheduler
    • M:N 两级线程调度模型
      • P-M 分离
      • G-M 绑定
    • Scheduler Workflow
      • Steal(任务窃取)
      • Sysmon(系统监控)
  • CSP 并发模型
    • Channel 类型与操作符
    • Channel 缓冲区
    • Channel 遍历与关闭
  • 锁并发模型

GPM 调度模型

Golang Runtime 实现的 GMP 调度模型是在 Linux 两级线程调度模型(User Thread 和 LWP 混合的多对多模型)的基础上进行了改进,对 Linux LWP 和 Kernel Thread 进行了封装并引入了 G(Goroutine)、P(Processor)、M(Machine)、Golang Runtime Scheduler 等新的对象。

GPM 的本质是一种协作式的并发模型,它使用了特有的调度机制和栈切换机制,能够高效地并发执行多个任务,避免了线程阻塞和上下文切换的开销。

基本概念

G(Goroutine)

Goroutine 的本质是 Golang Runtime 抽象实现的一种函数实例,Goroutine 结构体具有自己的函数体、堆栈、执行状态、寄存器上下文和程序计数器等信息。

同时,Golang Runtime 还为 Goroutine 糅合了 “User Level Thread 线程调度" 和 “Co-routine 栈切换” 这两大机制:

  1. User Level Thread 线程调度:是一种用户态线程,由 User Process 负责创建和销毁,所有 Goroutine 实例共享同一个 User Process 的内存空间。多个 Goroutine 实例之间的执行采用了 M:N 两级线程调度模型。
  2. Co-routine 栈切换:具有 “主动让出、保存自身的状态、等候恢复“ 特性,这是一种 “主动的协作式" 调度机制。在 Goroutine 实例中可以设置某个 “主动让出“ 点(类似 Python 的 yield 语句),从而释放 Machine 去执行其他 Goroutine 实例。这一 Co-routine 特性只需要实现 “栈切换“,相对于 “线程切换“ 而言,有更快的速度和更小的开销。

可见,Goroutine 兼具了 User Level Thread 和 Co-routine 的特点,并与 GPM 的整体架构自洽,Goroutine 实例只有被存储到 Local Goroutine Queue 或 Global Goroutine Queue 时才会被调度。

Golang 原生支持高并发,使用 go 语句即可新建一个 Goroutine 实例。

package mainimport ("fmt""time"
)func say(s string) {for i := 0; i < 5; i++ {time.Sleep(100 * time.Millisecond)fmt.Println(s)}
}func main() {// go 函数名(形参列表)go say("world")say("hello")
}

P(Processor)

Processor 的本质是 Golang Runtime 抽象实现的一种 Goroutine Queue 和处理器执行环境的结合体。

  1. 一方面,以 Local Goroutine Queue 的方式来存储待执行 Goroutine 实例,类似于传统 Linux 的线程调度队列,与 Scheduler 配合将 Goroutine 调度到相应的 Machine 中执行;
  2. 另一方面,对 Machine 来说,Processor 结构体提供了相关的处理器执行环境信息,包括:内存分配状态,任务队列等;对于 Goroutine 来说,Machine 和 Processor 共同构成了 Goroutine 的执行环境。

Processor 的 Local Goroutine Queue 与 Global Goroutine Queue 的区别在于:Local Queue 有长度限制,不超过 256 个。新建 Goroutine 时,会优先选择 Local Queue,如果 Local Queue 满了,则将 Local Queue 的一半的 G 移动到 Global Queue,以此来实现调度资源的共享和再平衡。

M(Machine)

Machine 的本质是对 Kernel Thread 的封装,作为真正的可调度执行单元。Machine 首先会与 Processor 建立关联,然后不断地从 Local Goroutine Queue 或 Global Goroutine Queue 中获取 Goroutine 实例并调度到 CPU core 中执行。

当 Processor 被入队 Goroutine 实例时,就会触发创建或者唤醒一个 Machine 去执行。并且,每个 Machine 在同一时刻只能执行一个 Goroutine 实例,但是多个 Goroutine 实例可以在多个 Machine 上并发地执行。

Golang Runtime Scheduler

Scheduler 本质是 Golang Runtime 抽象实现的一种 Goroutine 调度器,类似 Linux Kernel 种的线程调度器。

当正在 Machine 中执行的某个 Goroutine 实例阻塞时,Scheduler 会将该 Machine 分配给同一 Queue 中的其他 Goroutine 实例,从而避免了某个 Goroutine 阻塞导致的 Machine 资源浪费。

M:N 两级线程调度模型

G、P、M 三者组合实现了 Golang 特别的 M:N 两级线程调度模型,其中包含了以下细节:

  • 映射关系

    • G、P 是多对多映射关系;
    • P、M 是一对一映射关系;
  • 绑定关系
    • P、M 之间并没有绑定关系
    • M、Kernel Thread 之间存在绑定关系。
  • 数量关系
    • P 的数量是固定的,由物理环境决定的,可以通过修改环境变量 GOMAXPROCS 来设置,要小于等于 Linux Processor(逻辑处理器)的数量。
    • M 的数量不受限于 Linux Processor 数量的限制,如 Kernel Thread 一般。当没有足够的 M 来执行 G 时,Runtime 就会自动创建出新的 M;
  • 两级调度
    • 一级调度:G 到 P 的调度。
    • 二级调度:M 到 CPU 的调度。
  • M:N(多对多):G、M 之间是多对多调度关系。

P-M 分离

M 在执行 G 时必须映射到一个 P,没有映射到 P 的 M 处于空闲状态。P、M 分离增加了架构的扩展性,为了保证 P 中的 G 能够得到及时执行。如下图所示:

  1. 当 M 被阻塞时,M 就会释放 P,然后将 P 映射到空闲的 M 上。例如:当 G0 此时因为网络 I/O 而阻塞了 M,那么 P 就会携带剩余的 G 映射到 M1 中。M1 可能是新创建的,也可能是 Scheduler 从空闲 M 列表中分配的。

  2. 当 M 对应的 Kernel Thread 被唤醒时,M 将会尝试为 G0 捕获一个 P 上下文。此时 M 会从 Schduler 的空闲 P 列表中获取,如果获取不成功,M 会被 G0 放入到 Schduler 的可执行 G 队列中,等待其他非空闲 P 的查找。

G-M 绑定

通常的,G-M 是分离解耦的状态。特别的 G-M 绑定功能,专用于某些要求固定在一个线程上运行的程序,需要通过 lockOSThread 和 unlockOSThread 来实现。

处理流程如下:

  1. G_a 锁定 M0 lockOSThread。
  2. G_a 调用 gosched 切走,投入 P1 队列。
  3. M0 调度,发现是 lockedm,于是让出 P0,自己调用 notesleep 睡眠。
  4. M1 取出 G_a,发现是 lockedg,于是让出 P1 给 M0,并且唤醒 M0,自己变 idle,stopm 休眠。
  5. M0 继续执行 G_a。

最终效果是,G_a 只在 M0 上运行,锁定这段期间,M0 也只执行了 G_a 的任务。

Scheduler Workflow

Goroutine 实例在 GPM 调度模型中处理流程如下所示:

  1. 当 Golang Runtime 执行 go func() 语句时,新建一个 G 实例。
  2. 新建的 G 会被放入 P 的 Local Queue 或 Global Queue 中,进入等待执行的状态。
  3. P 唤醒或创建 M 以执行 G。
  4. M 不断地进行事件循环,寻找在可用状态下的 G 并执行其任务(func)。
  5. M 执行完 G 并清除
  6. 清除后,M 重新进入事件循环。

Steal(任务窃取)

Steal(任务窃取)的作用是为了保证 G 的均衡执行。当 M 执行 G 完毕后,P 会将 G 从 Local Queue 中弹出,同时 P 会检查当前的 Local Queue 是否为空。如果为空,则会先从 Global Queue 窃取 G,如果没有获取到,然后再考虑随机地从其他 P 的 Local Queue 中尝试窃取一半可运行的 G。

如下图所示,P2 在 Local Queue 中找不到可以运行的 G,它就会执行 work-stealing 调度算法,随机选择其它的 P,例如 P1,并从 P1 的 Local Queue 中窃取了三个 G(一半)到 P2 自己的 Local Queue 中。至此,P1、P2 都拥有了可运行的 G,P1 多余的 G 也不会被浪费,调度资源将会更加平均的在多个处理器中流转。

Sysmon(系统监控)

Sysmon(系统监控)是一个特殊的 M,但 sysmon 不会映射到 P,只作用于监控一些阻塞的异常情况,比如有一个 M 长时间阻塞超过 10ms,那么 sysmon 会强制把 M-P 解映射,把 M 游离出去,同时让 P 映射到一个新的空闲 M 上,继续执行队列里的 G 任务。

CSP 并发模型

CSP(Communicating Sequential Processes,通信顺序进程)并发模型最早在 1977 年由 Tony Hoare 发表的论文提出,它倡导使用通信的手段来进行共享内存,继而实现多个线程之间的通信。这也是 Golang 倡导使用的并发模型,通过 Channel 来使用。

CSP 有两个核心概念:

  1. 并发实体:在 Golang 中就是 Goroutine,它们相互独立,且并发执行;
  2. 通道(Channel):并发实体之间使用 Channel 发送信息。

Golang 实现的 CSP 并发模型最大的特征就是 Goroutine 之间没有使用共享的内存空间,而是使用 Channel 来进行数据交换,传输具有类型的消息。并发实体在通道中发送数据或接受数据都会让 Goroutine 的阻塞,直到 Channel 中的数据被发送或接受完成。Goroutine 之间通过这种方式实现交互及同步。

可见,CSP 类似于同步队列(会阻塞),关注的是消息传输的方式,发送和接收信息的 Goroutine 可能不知道对方是谁,它们之间是互相解耦的。另外,Channel 与 Goroutine 也不是紧耦合的,Channel 作为独立的对象,可以被任意的创建、释放、读取、放入数据,并在不同的 Goroutine 中传递使用。

Channel 的特性给并发编程带来了极大的灵活性,但是 Channel 也很容易导致死锁,如果一个 Goroutine 在读取一个永远没有数据放入的 Channel 或者把数据放入一个永远不会被读取的 Channel 中,那么它会将被永远阻塞。

Channel 类型与操作符

Channel 的本质是一种用来传递数据的数据结构,在两个 Goroutine 之间传递一个具有特定类型的数据,以此来同步运行及通讯。

Channel 使用操作符 <-,形象的指定了通道的方向,根据位置的不同表示发送或接收。如果未指定方向,则表示为双向通道。例如:

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据并把值赋给 v

使用 chan 关键字来定义一个 Channel 变量:

ch := make(chan int)

需要注意的是,默认情况下,Channel 是不自带缓冲区的。发送端发送数据,就必须同时存在接收端接收相应的数据。

  1. 示例 1:通过两个 Goroutine 来计算数字之和,在完成计算任务后,通过 Channel 来传输结果:
package mainimport "fmt"func sum(s []int, c chan int) {sum := 0for _, v := range s {sum += v}c<- sum    // 把 sum 发送到通道 c
}func main() {s := []int{7, 2, 8, -9, 4, 0}c := make(chan int)go sum(s[:(len(s) / 2)], c)go sum(s[(len(s) / 2):], c)x, y := <-c, <-c    // 从通道 c 中接收fmt.Println(x, y, x+y)
}
  1. 示例 2:生产者每秒生成一个字符串,并通过 Channel 传输给消费者,生产者使用两个 Goroutine 并发运行,消费者在 main() 函数的 Goroutine 中进行处理。
package mainimport ("fmt""math/rand""time"
)// 数据生产者
func producer(header string, channel chan<- string) {// 无限循环, 不停地生产数据for {// 将随机数和字符串格式化为字符串发送给通道channel <- fmt.Sprintf("%s: %v", header, rand.Int31())// 等待1秒time.Sleep(time.Second)}
}// 数据消费者
func customer(channel <-chan string) {// 不停地获取数据for {// 从通道中取出数据, 此处会阻塞直到信道中返回数据message := <-channel// 打印数据fmt.Println(message)}
}func main() {// 创建一个字符串类型的通道channel := make(chan string)// 创建producer()函数的并发goroutinego producer("cat", channel)go producer("dog", channel)// 数据消费函数customer(channel)
}

运行结果:

dog: 2019727887
cat: 1298498081
dog: 939984059
cat: 1427131847
cat: 911902081
dog: 1474941318
dog: 140954425
cat: 336122540
cat: 208240456
dog: 646203300

整段代码中,没有线程创建,没有线程池也没有加锁,仅仅通过关键字 “go” 实现 goroutine,和 channel 实现数据交换。

Channel 缓冲区

Channel 可以显式地设置缓冲区,缓冲区就类似于一个消息队列。带缓冲区的 Channel 允许发送端的数据发送,和接收端的数据接收处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,然后等待接收端去获取数据,而不是要求接收端立刻去获取数据。

如果 Channel 不设置缓冲区,那么发送端就会阻塞,直到接收端从 Channel 中接收了值。如果 Channel 带缓冲,那么发送端则会阻塞,直到发送的值被拷贝到缓冲区内。如果 Channel 的缓冲区已满,则意味着需要等待直到某个接收端获取到一个值。接收端在有值可以接收之前会一直阻塞。

使用 make() 的第二个参数来指定缓冲区大小,但需要注意的是,缓冲区的大小是有限的,所以还是必须要有接收端来接收数据,否则缓冲区一满,数据发送端就无法再发送数据了。

ch := make(chan int, 100)

示例:

package mainimport "fmt"func main() {// 这里我们定义了一个可以存储整数类型的带缓冲通道,缓冲区大小为 2。ch := make(chan int, 2)// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据,而不用立刻需要去同步读取数据。ch <- 1ch <- 2// 获取这两个数据fmt.Println(<-ch)fmt.Println(<-ch)
}

Channel 遍历与关闭

通过 range 关键字还可以用于遍历 Channel 变量,以遍历的方式来读取数据。如果 Channel 接收不到数据,那么 ok 变量为 false,这时 Channel 变量就可以使用 close() 函数来关闭。

package mainimport "fmt"func fibonacci(n int, c chan int) {x, y := 0, 1for i := 0; i < n; i++ {c <- xx, y = y, x + y}close(c)
}func main() {c := make(chan int, 10)go fibonacci(cap(c), c)/*** range 函数遍历每个从通道接收到的数据,* 因为 c 在发送完 10 个数据之后就关闭了通道,* 所以这里我们 range 函数在接收到 10 个数据之后就结束了。* 如果上面的 c 通道不关闭,那么 range 函数就不会结束,从而在接收第 11 个数据的时候就阻塞了。*/for i := range c {fmt.Println(i)}
}

锁并发模型

锁并发模型是高级编程语言最常规的多线程并发模型,依赖共享内存,程序的正确运行很大程度依赖于开发人员的能力和技巧,程序在出错时也不易排查。

Golang 除了 CSP 之外也同样支持协程锁,用于保证执行 Goroutine 的时候不阻塞 M。例如:任务 A 需要修改 Z,任务 B 也需要修改 Z。如果是串行系统,A 执行完了,再执行B,很简单。但在并发系统中,因为 A,B 是并发执行的,所以就需要在操作 Z 的时候确保 A、B 保证串行化的机制。

CO_LOCK
{// 处理逻辑
}
CO_UNLOCK

如下图所示:

  1. A 要修改 Z,所以 A 加了协程锁。
  2. 加锁之后,由于处理一些其他的逻辑,例如等待某些事件,又把 CPU 切到 M.g0 调度了(yield),并且此时没有放锁。
  3. 这时 M 把 B 拿过来执行,yield to B。
  4. B 也要修改 Z,但此时发现 A 已经对 Z 加锁了,于是 B 把自己挂到锁结构里面去。
  5. 然后 B 直接切走,yield to M.g0。
  6. 现在 A 的事件到达,M.g0 重新调度到 A 执行,yield to A。
  7. A 从刚刚切走的地方开始执行,完成后放锁。注意,A 方锁时,就会把 B 从锁队列中摘除,重新加到 M 的调度队列中。
  8. A 方锁后,M.g0 调度 B 执行。
  9. B 从刚刚加锁的地方唤醒,然后对 Z 加锁。然后走锁内逻辑后,放锁。

以上就是协程锁的实现原理。保证 A、B 在修改 Z 的时候必须串行化。

Go 语言编程 — GPM 与 CSP 高并发模型相关推荐

  1. Golang语言快速上手到综合实战(Go语言、Beego框架、高并发聊天室、豆瓣电影爬虫) 下载

    下载Golang语言快速上手到综合实战(Go语言.Beego框架.高并发聊天室.豆瓣电影爬虫) 下载地址:请加QQ:397245854 Go是Google开发的一种编译型,可并行化,并具有垃圾回收功能 ...

  2. linux网络编程(二)高并发服务器

    linux网络编程(二)高并发服务器 错误处理 高并发服务器 多进程并发服务器 客户端 错误处理 #include "wrap.h"int Bind(int fd, const s ...

  3. 【项目学习】C++实现高并发服务器——代码学习(一)Reactor高并发模型

    项目来源:WebServer 上一篇:环境搭建 本文介绍以下功能的代码实现 利用IO复用技术Epoll与线程池实现多线程的Reactor高并发模型: 一.IO复用技术 IO多路复用使得程序能同时监听多 ...

  4. 27.Linux网络编程socket变成 tcp 高并发 线程池 udp

    好,咱们开始上课了,从今天开始咱们连续讲 8 天的,网络编程这个还是在linux环境下去讲,咱们先看一下咱们这 8 天都讲什么东西,跟大家一块来梳理一下,你先有个大概的印象,这些你也不要记,那么网络编 ...

  5. 高并发编程(四)高并发解决方案从前端到数据库

    1. 高并发和大流量解决方案 高并发架构相关概念 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理 ...

  6. c语言编程大体难度较高的,面向初学者的C语言编程方法研究

    李欣潼 摘要:C语言是一门十分重要但对初学程序设计的同学来说又是相对难学的一门计算机语言.从初学者的角度出发,按照分析问题,算法设计,编码实现及程序运行几个步骤,说明了学习C语言的方法.论文以二元一次 ...

  7. 证券期货交易高并发模型

    为什么80%的码农都做不了架构师?>>>    交易系统中的产品和产品之间是隔离的,产品之间的消息不共享,也不会造成干扰. 同一个产品下的订单必须顺序处理,但不同的产品之间的订单没有 ...

  8. Linux网络编程(六)-高并发服务器03-I/O多路复用03:epoll【红黑树;根节点为监听节点】【无宏FD_SETSIZE限制;不需每次都将要监听的文件描述符从应用层拷贝到内核;不需遍历树】

    一.epoll概述 epoll的本质是一个[红黑树].监听结点为根节点. 大量并发,少量活跃效率高. epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并 ...

  9. java高并发抢单编程模型_Java高并发模型

    package MainFixedFuc; import java.util.concurrent.ExecutorService; import java.util.concurrent.Execu ...

最新文章

  1. MySQL数据库排序order by(asc、desc)
  2. 中南大学计算机辅助工艺设计,中南大学计算机辅助制造大作业.doc
  3. [轉]MS SQL Server启用AWE用查看内存使用情况
  4. 【Nutch基础教程之七】Nutch的2种运行模式:local及deploy
  5. 《JavaScript高级程序设计》笔记总结
  6. 带有Java Util日志记录的Java 8延迟调用
  7. mybatis plus 导出sql_软件更新丨mybatis-plus 3.0.7 发布,辞旧迎新
  8. 飞鸽传书2008一种重要心态
  9. imx6ull EMMC和NABD 的移植注意事项,差别
  10. mysql的jar包文件在哪找_数据库的jar在哪找
  11. Java多线程学习笔记(三)——Future和FutureTask
  12. WPS如何并排放置两张图片_Animate如何制作文字动图动画
  13. 国产在线三维云CAD:CrownCAD (在线建模CAD软件)
  14. 《华为工作法》8 自我提升的华为人
  15. win7 install solution for intel SKL and BSW platform
  16. 数据结构课程设计——逆波兰表达式的计算
  17. Ubuntu下制作.deb安装包之dkpg
  18. wget下载到一半断了,重连方法
  19. Quartz源码解读-任务是如何定时执行的
  20. 12306 java程序_基于java httpclient的12306 买票软件

热门文章

  1. 继云盘精灵关闭后,又一云盘宣布关闭
  2. Java爬虫实践之获取历史上的今天
  3. ARChon 分析之六:native-client 的加载、显示与事件传递浅析
  4. dell主板40针开机针脚_戴尔 OptiPlex 390 790 990主板34针前置 面板针脚 接口定义
  5. bim软件功能划分可以分为几类?用于revit的出图插件
  6. WB8使用说明-基础(引用)
  7. 浅谈微软补丁安全更新公告
  8. linux下以M为单位显示文件大小
  9. PHP 使用 PhpSpreadsheet
  10. 深度学习实战 第6章卷积神经网络笔记