When g yields it’s enqueued to global schedt.runq because if we yield we are not highly prioritized (globrunqput)

When g readies another g', g' is enqueued to local p.runq without locking (runqput) (see Fairness)
If local p.runq is full, we enqueue batch (half of gs) to global schedt.runq, because it means that we have to much work on our p. Note that in that case we need to hold the lock schedt.lock for global schedt.runq (contended slow case)

Function schedule schedules a goroutine. It calls findRunnable to find next g to run:

  1. Call runqget to get g from p.runq and run it
  2. Call globrunqget. It gets g from global schedt.runq acquiring the lock schedt.lock (contended slow case) and also moves the batch of gs from schedt.runq to p.runq to minimize the # of accesses to schedt.runq
  3. Poll network
  4. Steal batch (half of gs) from others (random) ps by calling runqsteal (work stealing)
  5. Sleep

Local p.runq have a single producer but multiple consumers, because there can be multiple ps that steal work from our p.runq. Thus, when enqueueing to p.runq we can use atomic store but when dequeueing we need to use spinlocks (CAS)

Because findRunnable would starve schedt.runq if p.runq is always non empty, schedule tries to dequeue one g from schedt.runq in iteration of scheduling