golang的协程调度是抢占式的吗?
操作系统的调度方式可以分为抢占式(Preemptive)和非抢占式(Non-preemptive,也称为协作式)两种。它们的区别主要体现在任务(进程或线程)被调度的时机以及调度的控制权如何分配。
抢占式调度
在抢占式调度中,操作系统具有较高的优先级,可以在任何时刻中断当前正在执行的任务,并强制将控制权交给另一个任务。也就是说一个正在执行的任务可能会被更高优先级的任务抢占,即使该任务尚未自愿释放控制权。这种方式可以确保高优先级任务及时响应,但也可能导致低优先级任务被频繁中断,影响系统的稳定性和可预测性。
非抢占式调度
在非抢占式调度中,任务只有在自愿释放控制权(例如等待I/O操作完成、进入休眠状态等)或者任务结束时,才会进行任务切换。操作系统无法强制中断当前正在执行的任务。这种方式可以确保低优先级任务不会被频繁中断,但也可能导致高优先级任务无法及时响应,特别是当某个任务陷入无限循环或阻塞状态时。
因此,抢占式和非抢占式就是看操作系统支不支持 优先级 这个概念,有优先级的话,高优先级的进程可以打断正在运行的低优先级进程,所以是抢占式的。没有优化级的话,进程只有自己让度CPU(如时间片到了,或者阻塞等),所以是非抢占式的。我们知道现在的操作系统都设置了 nice值,因此基本上都是抢占式的调度。
抢占式和协作式虽然说是操作系统的概念,但是golang的协程调度也是任务调度问题,那么它是抢占式的还是协作式的呢?
协程是用户态线程,我们可能会理所当然的认为go的协程调度就是非抢占式的,因为协程调度的GPM模型是由用户态主动让出执行线程的(让度条件就是自己的goroutine执行完了,或者pending), 协程没有优化级的概念。
但真是这样的吗?其实不然,从 go 1.14 开始,go 调度器是非合作抢占的。每个 goroutine 在一定的时间片后被抢占。在 go 1.19.1 中是 10ms 源码链接。就算该goroutine一直在占用CPU进行计算,只要10ms的时间到了,它依然会被调度出去重新丢到等待队列里面。
1// forcePreemptNS is the time slice given to a G before it is
2// preempted.
3const forcePreemptNS = 10 * 1000 * 1000 // 10ms
4
5func retake(now int64) uint32 {
6 n := 0
7 // Prevent allp slice changes. This lock will be completely
8 // uncontended unless we're already stopping the world.
9 lock(&allpLock)
10 // We can't use a range loop over allp because we may
11 // temporarily drop the allpLock. Hence, we need to re-fetch
12 // allp each time around the loop.
13 for i := 0; i < len(allp); i++ {
14 _p_ := allp[i]
15 if _p_ == nil {
16 // This can happen if procresize has grown
17 // allp but not yet created new Ps.
18 continue
19 }
20 pd := &_p_.sysmontick
21 s := _p_.status
22 sysretake := false
23 if s == _Prunning || s == _Psyscall {
24 // Preempt G if it's running for too long.
25 t := int64(_p_.schedtick)
26 if int64(pd.schedtick) != t {
27 pd.schedtick = uint32(t)
28 pd.schedwhen = now
29 } else if pd.schedwhen+forcePreemptNS <= now {
30 preemptone(_p_)
31 // In case of syscall, preemptone() doesn't
32 // work, because there is no M wired to P.
33 sysretake = true
34 }
35 }
36 if s == _Psyscall {
37 // Retake P from syscall if it's there for more than 1 sysmon tick (at least 20us).
38 t := int64(_p_.syscalltick)
39 if !sysretake && int64(pd.syscalltick) != t {
40 pd.syscalltick = uint32(t)
41 pd.syscallwhen = now
42 continue
43 }
44 // On the one hand we don't want to retake Ps if there is no other work to do,
45 // but on the other hand we want to retake them eventually
46 // because they can prevent the sysmon thread from deep sleep.
47 if runqempty(_p_) && atomic.Load(&sched.nmspinning)+atomic.Load(&sched.npidle) > 0 && pd.syscallwhen+10*1000*1000 > now {
48 continue
49 }
50 // Drop allpLock so we can take sched.lock.
51 unlock(&allpLock)
52 // Need to decrement number of idle locked M's
53 // (pretending that one more is running) before the CAS.
54 // Otherwise the M from which we retake can exit the syscall,
55 // increment nmidle and report deadlock.
56 incidlelocked(-1)
57 if atomic.Cas(&_p_.status, s, _Pidle) {
58 if trace.enabled {
59 traceGoSysBlock(_p_)
60 traceProcStop(_p_)
61 }
62 n++
63 _p_.syscalltick++
64 handoffp(_p_)
65 }
66 incidlelocked(1)
67 lock(&allpLock)
68 }
69 }
70 unlock(&allpLock)
71 return uint32(n)
72}

评论列表:
暂无评论 😭