当前位置: 首页 > 后端技术 > PHP

经典面试题:你认为Go什么时候会抢占P?

时间:2023-03-30 02:40:08 PHP

微信搜索【脑补炸鱼】关注这条炸肝炸鱼。本文GitHubgithub.com/eddycjy/blog已收录,附有我的系列文章、资料和开源Go书籍。大家好,我是炸鱼。前几天我们讲了《单核 CPU,开两个 Goroutine,其中一个死循环,会怎么样?》的问题,我们在一个细节部分提到了它:新朋友会有更多的疑问,就是Go语言中如何抢占P,这里Howdoyoudoit?在今天的文章中,我们将解密P的抢占。调度器的发展历史在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方法,它处理以下两种场景:抢占阻塞在系统调用上的P。抢占运行时间过长的G。这里主要关注抢占P的场景,分析如下:funcretake(nowint64)uint32{n:=0//防止变化,锁住所有Plock(&allpLock)//进入主逻辑,开始i:=0的所有P循环处理;我0&&pd.syscallwhen+10*1000*1000>now{continue}...}}unlock(&allpLock)returnuint32(n)}第二种场景着重于这一长串的判断:runqempty(_p_)==true方法会判断任务队列是否P为空,以检测是否还有其他任务需要执行。atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle)>0会判断是否有空闲的P和正在调度窃取G的P。pd.syscallwhen+10*1000*1000>现在会判断系统调用时间是否超过10ms。这里比较奇怪的是,runqempty方法已经明确判断没有其他任务了,也就是说没有任务可以执行,也就不需要抢P了。但是实际情况是最后还是希望继续持有P,因为有可能阻止sysmon线程的深度睡眠。完成以上判断后,进入抢P阶段:funcretake(nowint64)uint32{fori:=0;我