11 вопросов
Модель планировщика Go: G (goroutine) - горутина, M (machine) - поток ОС, P (processor) - контекст выполнения, связка с очередью горутин. M выполняет G на P; P привязан к M. Несколько G могут стоять в очереди на P; количество P обычно равно GOMAXPROCS.
G - горутина (структура g): стек, функция, состояние. M - поток ОС (thread), выполняет код Go. P - логический процессор (структура p): очередь локальных горутин, кеш и т.д. У каждого P может быть свой M; M без P не выполняет пользовательский код.
У каждого P - локальная очередь горутин (runqueue). Есть глобальная очередь (редко используется при нормальной загрузке). При нехватке работы P забирает половину горутин у другого P (work stealing). Новые горутины обычно попадают в локальную очередь P создателя.
Когда у P пустая локальная очередь, он пытается "украсть" половину горутин из очереди другого P или взять из глобальной очереди. Балансирует нагрузку между ядрами без центральной очереди. Уменьшает простаивание P.
Компонент рантайма: интегрирует с сетевым I/O (epoll, kqueue, IOCP). Когда горутина блокируется на сетевой операции, она снимается с M и не занимает поток; при готовности события поллер будит горутину и ставит в очередь. Потоки не блокируются на сети.
M в режиме "крутится" (spinning), когда ищет готовую к выполнению горутину вместо того, чтобы уходить в сон. Небольшое число spinning M ускоряет реакцию на появление новых горутин. Слишком много spinning - лишняя нагрузка на CPU.
Когда горутина блокируется (например, на канале или системном вызове), P может передать (hand off) другую горутину из своей очереди на тот же M или освободить P для другого M. Handoff - передача работы между M и P при блокировках.
С Go 1.14 планировщик вытесняет долго выполняющиеся горутины без точек вызова (tight loop), чтобы другие горутины получили время. Раньше кооперативная многозадачность могла оставлять одну горутину надолго на P. Preemption основан на асинхронных сигналах и проверках в точках вызова.
Периодический вывод в stderr: количество горутин, потоков (M), процессоров (P), состояние планировщика. Помогает увидеть рост числа горутин, простаивание P и общую картину. Для детального разбора используют scheddetail вместе с schedtrace.
GODEBUG=schedtrace=1000 ./appruntime.Gosched() - добровольно отдать текущий квант времени: текущая горутина уходит в очередь, планировщик выбирает другую. runtime.LockOSThread() привязывает горутину к текущему потоку ОС до UnlockOSThread; нужен для C-библиотек, требующих один поток, или для низкоуровневых задач.
runtime.Gosched()
runtime.LockOSThread()
defer runtime.UnlockOSThread()M создаются при блокировке в системном вызове (syscall): текущий M блокируется, рантайм может создать новый M для привязки к освободившемуся P и продолжения выполнения горутин. После возврата из syscall старый M может вернуться в пул. Поэтому число M не ограничено числом P.