goroutine的一些小知识
进程
1.操作系统程序的最小单位
2.进程用来占用内存空间
3.进程相当于一个个厂房,占用工厂的空间
线程
1.线程用来占用CPU时间
2.线程的调度需要由系统进行,开销较大
3.线程相当于工厂的生产线,占用工人的工时
4.线程里跑的程序就是生产流程
5.问题
- 线程本身占用资源大
- 线程的操作开销大
- 线程切换开销大
协程
1.协程就是将一段程序的运行状态打包,可以在线程之间调度
2.将生产流程打包,使得流程不固定在生产线上
3.协程并不取代线程,协程也要在线程上运行
4.线程是协程的资源,协程使用线程这个资源
5.优势
- 资源利用
- 快速调度
- 超高并发
细说协程
协程的本质runtime.g结构体
type g struct {
stack stack // 协程堆栈地址
sched gobuf //goroutine切换时,用于保存g的上下文,用于保存程序的运行现场
atomicstatus atomic.Uint32 //协程的状态
goid uint64 //goroutine的id
...
...
}
stack:堆栈地址,包括low和high两个上下界
gobuf:目前程序的运行现场
- sp:栈地址,管理现在用哪个栈
- pc:程序计数器,程序运行到哪一行了
atomicstatus:协程状态
go描述线程的结构体runtime.m结构体
g0:g0协程,操作调度器
curg:current g,目前线程运行的协程g
mOS:不同操作系统线程的信息
单线程循环
-
线程首先执行g0栈调用schedule()方法
-
去g队列里拿到一个可以获取到的业务的g栈协程
-
schedule()方法调用了execute()
-
然后execute()调用gogo()方法,会插入一个goexit栈帧
-
然后根据g栈的gobuf里的sp和pc去执行业务,业务执行完毕
-
调用goexit方法,goexit()会调用goexit1(),goexit1()会调用goexit0()
-
goexit0()里面会调用mcall去切换g栈和g0栈,然后切换到g0栈后,继续执行schedule()方法,继续下一个循环
- 操作系统并不知道Goroutine的存在
- 操作系统线程执行一个调度循环,顺序执行Goroutine
- 调度循环非常像线程池
- 问题1:协程顺序执行,无法并发
- 问题2:多线程并发时,会抢夺协程队列的全局锁
本地队列解决了上述问题1。runtime.p结构体
m->M 线程
p的作用:
-
M与G之间的中介
-
P持有一些G,使得每次获取G的时候不用从全局找
-
大大减少了并发冲突的情况
-
如果在本地或者全局队列中都找不到G,去别的P中”偷“,增强了线程的利用率
新建协程
- 随机寻找一个P
- 将新协程放入P的runnext(插队)
- 若P本地队列已满,则会放入全局队列
如果G占用时间很长,会保存现场,触发切换,如果该g还需要继续执行,那么放入本地队列里,以便后续循环执行,如果需要休眠,那么暂时不放入本地队列
如果本地队列有一个很长执行时间的g,那么还可能造成全局队列饥饿问题
解决方案是,每过段时间会从全局队列拿一个G放入本地队列,具体是61一次线程循环,从全局拿一个
切换时间
-
主动挂起
-
系统调用完成时
总结
- 如果协程顺序执行,会有饥饿问题
- 协程执行中间,将协程挂起,执行其他协程
- 完成系统调用时挂起,也可以主动挂起
- 防止全局队列饥饿,本地队列随机抽取全局队列
抢占式调度
问题:
- 永远都不主动挂起
- 永远都不系统调用
- 且这个G调度时间很长
runtime.morestack方法
只要有函数调用跳转,编译的时候,就会插入该方法。该方法本意是检查协程栈是否有足够的空间
标记抢占
1.系统监控到Goroutine运行超过10ms时
2.将g.stackguard0置为0xfffffade抢占标记
抢占
1.执行morestack()的时候判断是否被抢占
2.如果被抢占,回到schedule()
基于信号的抢占式调度
如果协程是一个for死循环,没有函数调用跳转,也没有主动调用gopark的操作,也没有系统调用,怎么搞?
线程信号
- 操作系统有很多基于信号的底层通信方式
- 比如SIGPIPE、SIGURG、SIGHUP
- 线程可以注册对应信号的处理函数
信号的抢占式调度
- 注册SIGURG信号的处理函数
- GC工作时,向目标线程发送信号
- 线程收到信号,触发调度
总结
- 基于系统调用和主动挂起,协程可能都无法调度
- 基于协作的抢占式调度:业务主动调用morestack()
- 基于信号的抢占式调度:强制线程嗲用doSigPreempt()
问题:实战中协程过多问题?
- 文件打开数限制
- 内存限制
- 调度开销大
解决方案
-
优化业务逻辑
-
利用channel缓冲区
- 利用channel缓存机制
- 启动协程前,向channel送入一个空结构体
- 协程结束,取出一个空结构体
-
协程池(tunny)
-
预创建一定数量的协程
-
将任务送入协程池队列
-
协程池不断取出可用协程,执行任务
-
慎用线程池
- go语言的线程,已经相当于池化了
- 二级池化增加系统复杂度
- go语言的初衷是希望协程即用即毁,不要池化
-
-
调整系统资源
总结
1.为什么用协程
- 协程用来精细利用线程
- 写成可以支撑超高并发
2.协程是什么
- 从runtime角度看,协程是一个可以被调度的g结构体
- 从线程角度看,协程是一段程序,自带执行现场
3.G-M-P模型
- 通过P结构体,达到了缓存部分G的目的
- P本质上是一个G的本地队列,避免全局并发等待
- 窃取式工作分配机制能够更加充分利用线程资源
4.协程并发
- 如果协程顺序执行,会有饥饿问题
- 协程执行中间,将协程挂起,执行其他协程
- 完成系统调用时挂起,也可以主动挂起
- 防止全局队列饥饿,本地队列随机抽取全局队列
5.抢占式调度
- 基于系统调用和gopark主动挂起,协程可能无法调度
- 基于写作的抢占式调度:业务主动调用morestack()
- 基于信号的抢占式调度:强制线程调用doSigPreempt()