- Goroutine和线程的区别
- GMP 模型
- Global Queue <> Local Queue 交互
- P <> M 交互
- 协程切换时机
- Context
- 同步原语
- sync.Mutex
- sync.RWMutex
- sync.Cond
- sync/semaphore
- Timer
- 调度器
- 系统监控
Goroutine和线程的区别
- 内存大小
- 每个线程的内存大小是固定的,通常被指定为2MB,对一些轻量操作,这个内存大小过大;对一些重操作(比如疯狂递归),这个内存大小又会过小。
- goroutine的内存大小是动态变化的,通常的初始值为2KB。
- 调度
- 线程调度发生在内核态,由内核scheduler进行,线程切换会发生用户态到内核态的切换。
- goroutine的调度模型称为m:n,即m个协程,n个线程,线程的调度权当然还在内核,但是具体一个协程由哪个线程执行,完全由Go scheduler指定,发生在用户态,而且协程的优点是可以主动yield当前占用的线程给其他协程。
- 标识符
- 每个线程在内核态都有一个线程标识符。
- goroutine没有类似的标识符,阻止程序访问thread-local variable。
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 上。
协程切换时机
- channel, 网络 IO, sync 包等阻塞操作。
- runtime.GoShed 方法被调用。
- time 包,一个 P 会持有一个 timer 优先队列,堆顶是最早到期的协程。
- 栈拷贝。
- 时间片耗尽,10 ms。
Context
context.Background
和 context.TODO
是两个指向 context.emptyCtx
结构体的指针。
- 一般使用
context.Background
即可。 - 如果被调用的函数需要传入
context.Context
,但是当前还不确定如何构造这个context
对象,那么可以传入一个context.TODO
,未来再做替换。
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 会在两个模块触发计时器,检查计时器堆的堆顶,运行计时器中保存的函数:
- 调度器调度时会检查处理器 P 中的计时器是否准备就绪。
- 系统监控 runtime.sysmon 会检查是否有未执行的到期计时器。
标准库中的计时器已经可以胜任大部分工作,但是在 10ms 这个粒度下,社区中没有很好的计时器实现。
调度器
调度器指 Go 中用来调度管理 GMP 模型的程序。
- 调度器调用
runtime.schedule
执行调度。 - 调度器调用
runtime.findrunnable
获取可执行的 Goroutine。 - 调度器调用
runtime.findfunnable
从其他处理器窃取计时器。
系统监控
runtime.sysmon
是 Go runtime 的一个独立后台任务,有自己的独立线程,周期性地执行如下操作:
- 抢占,防止有 goroutine 占据 CPU 过长时间。
- 触发垃圾回收。
- 触发网络轮询。
- 检查 timer。
- 检查死锁。