【GO面试精要】GMP并发模型、Goroutine

GMP并发模型

进程与线程与协程【GO面试精要】GMP并发模型、Goroutine

多个线程属于同一个进程并共享内存空间,线程之间的通讯基于共享的内存进行。

Go语言的调度器使用与CPU数量相等的线程来调度多个Goroutine。

为什么用Go语言?“

进程、线程存在问题:

  1. CPU高消耗
    • 切换线程上下文需要申请、销毁资源消耗时间高
  2. 内存高占用
    • 线程占用1M以上的内存空间

协程(Goroutine)的优点:

  1. 占用的内存更小(几kb)
    • 初始为2kb,如果栈空间不足则自动扩容
  2. 调度更灵活(runtime调度)
    • Go自己实现的调度器,创建和销毁的消耗非常小,是用户级。
  3. 抢占式调度(10ms)
    • 编译器插入抢占指令,函数调用时检查当前Goroutine是否发起抢占请求
  4. 1.14版本后支持基于信号的异步抢占(20ms)
    • 垃圾回收扫描栈时触发抢占调度
    • 解决抢占式调度因垃圾回收和循环长时间占用资源(无法执行抢占指令)导致程序暂停

GMP并发模型

GMP

【GO面试精要】GMP并发模型、Goroutine

图-GMP

G 需要在 M 上才能运行,M 依赖 P 提供的资源,P 则持有待运行的 G,M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。

G: 取 goroutine 的首字母,主要保存 goroutine 的一些状态信息以及 CPU 的一些寄存器的值

M: 取 machine 的首字母,它代表一个工作线程,或者说系统线程。G 需要调度到 M 上才能运行,M 是真正工作的人

P:取 processor 的首字母,为 M 的执行提供“上下文”,保存 M 执行 G 时的一些资源,例如本地可运行 G 队列,memeory cache 等。

你了解过GMP并发模型吗?“

GM老版调度器:

  1. 激烈的锁竞争
    • 从全局队列中获取G,需要加锁
  2. 局部性差
    • 比如当 G 中包含创建新协程的时候,M 创建了 G’,为了继续执行 G,需要把 G’交给 M’执行,也造成了很差的局部性,因为 G’和 G 是相关的,最好放在 M 上执行,而不是其他 M’。
  3. 系统开销大
    • 系统调用 (CPU 在 M 之间的切换) 导致频繁的线程阻塞和取消阻塞操作增加了系统开销。

M 想要执行、放回 G 都必须访问全局 G 队列,并且 M 有多个,即多线程访问同一资源需要加锁进行保证互斥 / 同步,所以全局 G 队列是有互斥锁进行保护的。
【GO面试精要】GMP并发模型、Goroutine

GMP新版调度器(记忆图-GMP

  1. 解决GM老版调度器的问题

  2. M(线程):N(协程)关系

    • 创建 M 个线程(CPU 执行调度的单位),之后创建的 N 个 goroutine 都会依附在这 M 个线程上执行。

    • 在同一时刻,一个线程上只能跑一个 goroutine。当 goroutine 发生阻塞时,runtime 会把当前 goroutine 调度走,让其他 goroutine 来执行。

  3. 任务偷取(work stealing)

    • 全局队列已经没有 G,那 m 就要执行 work stealing (偷取):从其他有 G 的 P 哪里偷取一半 G 过来,放到自己的 P 本地队列
  4. 让出执行权(hand off)

    • 某个G堵塞,线程释放绑定的P,把P转移给其它空闲线程

“go func() 执行过程?”

【GO面试精要】GMP并发模型、Goroutine

  1. go关键字创建一个goroutine入队,如果本地P队列满了则入队全局G队列
  2. 从P队列中队头的G交给M执行
  3. P有两个关键特性
    1. work stealing
    2. hand off

查看更多Github

上一篇:Linux下GCC编译器的安装


下一篇:用Rtools辅助安装基于C、C++、Fortran的源代码R包