为什么要有协程
进程
- 操作系统程序的最小单位
- 进程用来占用内存空间
线程
- 每个进程可以有多个线程
- 线程使用系统分配给进程的内存, 线程之间共享内存
- 线程用来占用CPU时间
- 现成的调度需要由系统进行, 开销较大
线程的问题
- 线程本身占用资源大
- 线程的操作开销大
- 线程切换开销大
协程
- 协程就是将一段程序的运行状态打包, 可以在线程之间调度
- 将生产流程打包, 使得流程不固定在生产线上
- 线程并不取代线程, 协程也要放在线程上运行
- 线程是协程的资源, 协程使用线程这个资源
协程的优势
- 资源利用
- 快速调度
- 超高并发
总结
- 进程用于分配内存空间
- 线程用来分配CPU空间
- 协程用来精细利用线程
- 协程的本质是一段包含了运行状态的程序
协程的本质
协程在Go语言中的本质就是一个g结构体(位于runtime2.go).
type g struct {
stack stack // 栈信息
// ...
sched gobuf // 目前程序运行现场
atomicstatus uint32 // 协程状态
goid int64 // 协程的ID号
}
type stack struct {
lo uintptr
hi uintptr
}
type gobuf struct {
// stack pointer, 栈指针, 指向栈用到了什么地方
sp uintptr
// program counter, 程序计数器, 运行到了哪条代码
pc uintptr
// ...
}
协程如何描述(runtime2.go下的struct m)
type m struct {
go *g // go语言第一个协程, 操作调度器
curg *g // 正在运行的协程
mOS // 针对操作系统所记录的线程信息(os_linux / windows / darwin ...)
}
协程如何执行
单线程循环(Go 0.x)
多线程循环(Go 1.0)
线程循环
- 操作系统并不知道Goroutine的存在
- 操作系统线程执行一个调度循环(Go代码或者会变组成),顺序执行Goroutine
- 调度循环非常像线程池 问题:
- 协程顺序执行, 无法并发
- 多线程并发时, 会抢夺协程队列的全局锁
总结
- 协程的本质是一个g结构体
- 结构体记录了协程栈、PC信息
- 最简情况下,线程执行标准调度循环,执行协程
G-M-P调度模型
前序的调度像是本地队列
type p struct {
m muintptr // 指向相关的线程, 服务于一个线程
// 可执行的协程队列, 可无锁访问
runqhead uint32 // 队头
runqtail uint32 // 队尾
runq [256]guintptr
runnext guintptr // 下一个可用的指针
}
如果本地协程执行完成, 获取全局的锁, 取全局拉取协程
P的作用
- M与G之间的中介(送料器)
- P持有一些G, 使得每次获取G的时候不用从全局找
- 大大减少了并发冲突的情况
任务窃取
本地(m)和全局的携程队列都没有任务, 可以去别的p中获取, 增加了线程利用率
新建协程
- 随机寻找一个P
- 将新协程放入P的runnext(插队)
- 如果P本地队列满, 放入全局队列
GMP模型减少了全局协程队列锁的获取