Golang 并发相关笔记

Posted on 2023年7月22日周六 技术

Goroutine和线程的区别

GOPMAXPROCS决定了一个Go程序可以使用多少个系统线程。

GMP 模型

  • G: Goroutine,一个协程。
  • M: Machine,一个线程。
  • P: Processor,负责管理协程的数据结构。

一个 P 绑定一个正在运行的线程 M,同时维护一个正在运行的协程 G,以及一个等待运行的协程队列 Local Queue。

Go 进程会维护一个全局唯一的协程队列 Global Queue,其中是不被特定 P 绑定的 Q。

Global Queue <> Local Queue 交互

一个 G 创建出新的 G' 时,会先尝试把 G 放入对应 P 的 Local Queue 中,如果对应 Local Queue 满了,那么 G‘ 和 Local Queue 中的一半协程都会被放入 Global Queue 中。

P 每调度 n 次,就尝试会从 Global Queue 的队头获取一个 G' 执行。

P 在一些时候也会从其他的 P' 上偷取 P' 中的 Local Queue 进行执行。

P <> M 交互

当一个线程 M 被系统阻塞时,P 会被绑定到一个新的 M 上。

协程切换时机

Context

context.Backgroundcontext.TODO 是两个指向 context.emptyCtx 结构体的指针。

context 包中的各种函数可以衍生出各种 context 对象,并对衍生出的对象注入相关信息,子对象会继承父对象带有的 value、取消机制等状态。

同步原语

sync.Mutex

有正常模式和饥饿模式,一旦有 Goroutine 超过 10ms 没能获取到锁,它就会将当前互斥锁切换到饥饿模式。

sync.RWMutex

知道读写锁被尝试获取写锁后,不会允许新的 Goroutines 获得读锁就好了。

sync.Cond

不常用,但在 for checkCond { wait } 的场景下,把 for 中的 wait 替换成 sync.Cond 可以避免自旋消耗 CPU,这也依赖其他控制 for 检查条件的 goroutines 在条件达成时会调用同一个 sync.Cond

注意在创建一个 sync.Cond 的时候,还需要传入一个 sync.Mutex,这个 mutex 的作用是用来保护临界区变量的,前面说到 sync.Cond 是用来在 goroutines 之间同步条件,这里面的条件涉及到的变量就需要用到前面的 mutex 保护起来,不然会有竞争条件。

下面通过调用流程来说明如何使用 mutex 和这个 mutex 变量初始化的 cond:

条件修改方:

  • cond.L.Lock() 等价于 mutex.Lock()
  • 修改条件
  • cond.Broadcast() / cond.Signal()
    • 内部先释放锁 cond.L.Unlock()。
    • 唤醒 `cond.notify` 链表中等待的 goroutine。
    • 重新获取锁 cond.L.Lock(),返回上层调用。
  • (optional) 继续修改条件
  • cond.L.Unlock() 等价于 mutex.Unlock()

条件检查方

  • cond.L.Lock() 等价于 mutex.Lock()
  • for 检查条件
  • cond.Wait()
    • 内部先释放锁 cond.L.Unlock()
    • 加入 `cond.notify` 链表,等待被唤醒
    • 重新获取锁 cond.L.Lock(),返回上层调用
  • cond.L.Unlock() 等价于 mutex.Unlock()

sync/semaphore

不常用,知道信号量和锁的区别就是一个是数值,一个是 bool 就好了。

Timer

GMP 模型中的每个 P 会维护一个计时器堆,堆是一个四叉堆,堆顶是最先到期的计时器。

Go 会在两个模块触发计时器,检查计时器堆的堆顶,运行计时器中保存的函数:

标准库中的计时器已经可以胜任大部分工作,但是在 10ms 这个粒度下,社区中没有很好的计时器实现。

调度器

调度器指 Go 中用来调度管理 GMP 模型的程序。

系统监控

runtime.sysmon 是 Go runtime 的一个独立后台任务,有自己的独立线程,周期性地执行如下操作: