当前位置: 首页 > 科技观察

嗯,你认为Go什么时候会抢占P?

时间:2023-03-13 17:21:15 科技观察

本文转载自微信公众号《我的脑子是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。大家好,我是炸鱼。昨天刚从长沙冲浪回来,准备写一篇“潇洒潇洒”的游记,分享一波美食+游记,五一去长沙的朋友有没有?前几天我们聊了《单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?》,我们在一个细节部分提到:新朋友会有更多的疑问,就是Go语言中如何抢占P,怎么做?今天我们就来解密这篇文章PreemptP.调度器的发展历史在Go语言中,Goroutine在早期并不是设计成抢占式的。早期,Goroutine只是在读写时触发调度切换,主动放弃,加锁等操作。这有一个严重的问题,就是垃圾回收器在进行STW的时候,如果有一个Goroutine一直在阻塞调用,垃圾回收器就会等他,不知道要等到什么时候。。。在这个情况下,抢占式调度解决问题。如果一个Goroutine运行时间过长,需要抢占解决。这门Go语言在Go1.2开始实现了抢占式调度器,并不断完善到今天:Go0.x:asingle-threadedscheduler。Go1.0:基于多线程的调度器。Go1.1:基于任务窃取的调度器。Go1.2-Go1.13:基于协作的抢占式调度程序。Go1.14:基于信号的抢占式调度器。调度器的一个新提议:非均匀内存访问调度(Non-uniformmemoryaccess,NUMA),但由于实现过于复杂,优先级不够高,一直没有提上日程。有兴趣的小伙伴可以参考DmitryVyukov提出的Go的NUMA-awarescheduler,dvyukov。为什么要抢占P?为什么要抢占P?说白了,你不抢,你就没机会跑,会被吊死。或者资源分配不均匀,这在调度器的设计上显然是不合理的。同本例://MainGoroutinefuncmain(){//模拟单核CPUruntime.GOMAXPROCS(1)//模拟Goroutine死循环gofunc(){for{}}()time.Sleep(time.Millisecond)fmt.Println(“我的脑子炸了”)}在老版本的Go语言中,这个例子会一直被屏蔽,无法重见天日。这是一个需要抢先的场景。但是有朋友可能会问,抢占了会不会有新的问题。因为本来使用P的M很酷(M会绑定P),没有P就没法继续执行。这其实没什么问题,因为Goroutine已经阻塞在系统调用上了,暂时不会有后续的新需求执行。但是如果代码跑了很久又能跑起来(业务中也允许长时间等待),也就是Goroutine从阻塞状态中恢复过来,期望继续运行。没有P怎么办?这个时候,这个Goroutine和其他Goroutine一样,可以先检查自己所在的M是否还绑定了P:如果有P,就可以调整状态继续运行。如果没有P,可以重新抢P,然后占领绑定P,为自己所用。也就是说,抢占P本身就是一种双向行为。你抢了我的P,我也可以抢别人的P继续跑。如何抢占P在解释完P被抢占的原因之后,我们再深挖一下,“他”是如何抢占到具体P的呢?这就涉及到上面提到的runtime.retake方法,它处理以下两种场景:PreemptPblockedonasystemcall。抢占运行时间过长的G。这里主要关注抢占P的场景,分析如下:funcretake(nowint64)uint32{n:=0//防止变化,锁住所有Plock(&allpLock)//进入主逻辑,开始循环对所有P的处理fori:=0;i0&&pd.syscallwhen+10*1000*1000>now{continue}...}}unlock(&allpLock)returnuint32(n)}th这长串的判断集中在两个场景:runqempty(_p_)==true方法会判断任务队列P是否为空,从而检测是否还有其他任务需要执行。atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle)>0会判断是否有空闲P和正在调度偷G的Ppd.syscallwhen+10*1000*1000>now会判断系统调用时间是否超过10ms。这里比较奇怪的是,runqempty方法已经明确判断没有其他任务了,也就是说没有任务可以执行,也就不需要抢P了。但是实际情况是最后还是希望继续持有P,因为有可能阻止sysmon线程的深度睡眠。完成以上判断后,进入抢P阶段:funcretake(nowint64)uint32{fori:=0;i