Запуск программы на Go выглядит как один вызов main, но до него рантайм успевает построить планировщик, создать потоки ОС, разложить горутины по очередям и инициализировать пакеты. Ниже — механика этих шагов по исходникам рантайма: в каком порядке что происходит, при каких условиях срабатывают ветки и какие пороги зашиты в код. Отдельно — что меняется в Go 1.26.
Запуск рантайма: от _rt0 до main
Последовательность bootstrapначальная инициализация рантайма до запуска кода пользователя зафиксирована в комментарии к schedinit. schedinit — это функция, которая готовит планировщик рантайма к работе до запуска пользовательского кода: она инициализирует блокировки и счётчики планировщика, подсистемы памяти, типов и отладки, определяет число процессоров P и только после этого позволяет миру продолжить работу. Без неё рантайм не смог бы ни ставить горутины в очередь, ни распределять их по потокам. Сначала вызывается osinit, затем schedinit, затем создаётся и ставится в очередь новая Gгорутина — единица планирования рантайма Go, после чего вызывается runtime·mstart, и уже эта новая G вызывает runtime·main.
Внутри schedinit порядок такой. Сначала инициализируются блокировки планировщика через lockInit, затем при включённой гонке — raceinit. Дальше устанавливается sched.maxmcount = 10000 — верхняя граница числа потоков ОС. Затем мир останавливается (worldStopped), ранние значения GODEBUG читаются через getGodebugEarly и при успехе разбираются parseRuntimeDebugVars. Далее идут подсистемы: ticks.init, moduledataverify, stackinit, randinit, mallocinit, cpuinit(godebug), maps.AlgInit. После этого вызывается mcommoninit(gp.m, -1) — регистрация текущего M, затем modulesinit, typelinksinit, itabsinit, stkobjinit, sigsave, goargs, goenvs, secure, checkfds, при необходимости повторный parseRuntimeDebugVars, finishDebugVarsSetup и gcinit.
Затем выделяется аварийный стек gcrash.stack = stackalloc(16384), при disableMemoryProfiling обнуляется MemProfileRate, вызываются mProfStackInit(gp.m) и defaultGOMAXPROCSInit(). Под sched.lock вычисляется число процессоров и вызывается procresize(procs), после чего мир снова запускается (worldStarted).
mstart — точка входа для новых MM — поток операционной системы, на котором рантайм исполняет горутины: в отличие от G, которая планируется рантаймом и может переезжать между потоками, M — это именно поток ОС, и новый M начинает свою работу с mstart. Она написана на ассемблере, использует ABI0, помечена TOPFRAME и вызывает mstart0, который настраивает границы стека и stackguard, а затем вызывает mstart1().
Функция main() в пакете runtime (не main пакета main) обнуляет mp.g0.racectx, задаёт maxstacksize (1000000000 на 64-бит, 250000000 на 32-бит) и maxstackceiling = 2 * maxstacksize, выставляет mainStarted = true, при наличии sysmon запускает newm(sysmon, nil, -1) через systemstack, делает lockOSThread() и проверяет, что выполняется на m0: иначе throw("runtime.main not on m0").
Рисунок. Порядок bootstrap-шагов рантайма от входа до пользовательского main.
Как schedinit настраивает планировщик и GOMAXPROCS
schedinit инициализирует блокировки планировщика через lockInit (sched.lock, sched.sysmonlock, sched.deferlock, sched.sudoglock и другие), а также allocmLock.init и execLock.init. Затем задаёт sched.midle.init(unsafe.Offsetof(m{}.idleNode)) и sched.maxmcount = 10000. Далее вызывается defaultGOMAXPROCSInit(), после чего под lock(&sched.lock) сохраняется sched.lastpoll.Store(nanotime()) и определяется число процессов.
Если strconv.ParseInt(gogetenv("GOMAXPROCS"), 10, 32) успешен и n > 0, то procs = int32(n) и выставляется sched.customGOMAXPROCS = true. Иначе procs = defaultGOMAXPROCS(numCPUStartup). Затем вызывается procresize(procs), при ошибке — throw("unknown runnable goroutine during bootstrap"), после чего unlock(&sched.lock) и worldStarted().
PP — логический процессор планировщика, на котором в каждый момент выполняется не более одной горутины; набор P определяет, сколько горутин может исполняться параллельно procresize меняет число процессоров планировщика: требует удержания sched.lock и остановленного мира, обновляет статистику sched.totaltime и sched.procresizetime, при необходимости расширяет allp и инициализирует новые P через pp.init(i).
defaultGOMAXPROCSUpdateEnable — функция, которая включает или выключает автоматический пересчёт GOMAXPROCS рантаймом в зависимости от настройки debug.updatemaxprocs: это отладочная переменная, задаваемая через GODEBUG; значение 0 означает, что автоматические обновления GOMAXPROCS отключены (например, в песочницах, где нельзя делать системные вызовы для пересчёта). При debug.updatemaxprocs == 0 функция вызывает updatemaxprocs.IncNonDefault() — счётчик метрики, фиксирующий, что используется нестандартное (отключённое) поведение, — и возвращается, ничего не запуская. Иначе она запускает фоновую горутину go updateMaxProcsGoroutine(), которая и выполняет периодический пересчёт GOMAXPROCS.
Инициализация пакетов и порядок init
Инициализация пакетов идёт через структуру initTask, описывающую набор init-функций одного пакета. Поле state принимает значения 0 (не инициализирован), 1 (идёт инициализация) и 2 (готово), а nfns задаёт число init-функций, за которыми следуют их адреса. doInit проходит по срезу задач и для каждой вызывает doInit1.
В doInit1 при state==2 происходит немедленный возврат, при state==1 вызывается throw("recursive call during initialization - linker skew"). В остальных случаях state выставляется в 1, проверяется, что nfns != 0 (иначе throw("inittask with no functions")), и в цикле по i от 0 до nfns-1 вычисляется адрес очередной функции, она загружается и вызывается по порядку; после завершения state становится 2.
Порядок между пакетами задаётся порядком элементов в срезах: doInit(runtime_inittasks) вызывается для рантайма, а затем в main идёт обход модулей в порядке связного списка moduledata — в порядке, в котором их инициализирует динамический загрузчик. Циклы инициализации на уровне переменных пакета описаны в спецификации: инициализация идёт пошагово, выбирая самую раннюю по порядку объявления переменную без зависимостей от неинициализированных, а переменные, оставшиеся неинициализированными к концу процесса, образуют циклы инициализации, и программа невалидна.
Функция gcd к механизму инициализации отношения не имеет: она вычисляет наибольший общий делитель алгоритмом Евклида и используется структурой randomEnum.
Как main запускает пользовательский код и завершает программу
main_main — это символ main.main, привязанный через //go:linkname. В main() после инициализации рантайма он вызывается косвенно (fn := main_main; fn()), потому что линкер не знает адрес пакета main при компоновке рантайма.
Трассировка init включается перед doInit: при debug.inittrace != 0 устанавливаются inittrace.id = getg().goid и inittrace.active = true. После завершения инициализации она отключается строкой inittrace.active = false — чтобы избежать накладных расходов на сбор статистики в malloc и newproc. Сразу после этого выставляется mainInitDone.Store(true) и закрывается канал close(mainInitDoneChan), сигнализируя cgocallbackg о завершении инициализации.
runExitHooks — обёртка над exithook.Run(code). В main() она вызывается при ASAN-проверке утечек (runExitHooks(0) перед lsandoleakcheck()), а также в конце, если ещё не вызывалась (if !exitHooksRun { runExitHooks(0) }). os_beforeExit привязан к os.runtime_beforeExit через //go:linkname и вызывается из os.Exit(0); он выполняет runExitHooks(exitCode) и при exitCode == 0 && raceenabled вызывает racefini().
Планировщик G-M-P: очереди и кража работы
runqput: runnext, локальная очередь, глобальная
runqput кладёт горутину в локальную очередь P. Если параметр next истинен, функция сначала пытается поместить G в слот pp.runnext — отдельную ячейку перед локальной очередью, где хранится одна горутина, которую P запустит следующей, раньше остальных элементов кольцевого буфера. Если слот был пуст, функция сразу возвращается; иначе старая горутина из runnext вытесняется и дальше обрабатывается как обычная.
Далее вычисляются h = pp.runqhead и t = pp.runqtail. Если t-h < uint32(len(pp.runq)), горутина кладётся в кольцевой буфер по индексу t%len(pp.runq), и runqtail увеличивается. Если очередь полна, вызывается runqputslow.
В runqputslow берётся батч размером len(pp.runq)/2, проверяется, что очередь действительно полна (иначе throw("runqputslow: queue is not full")), первые n горутин копируются в batch, runqhead сдвигается на h+n через CAS, затем в batch[n] кладётся сама горутина, и весь батч из n+1 горутин отправляется в глобальную очередь под sched.lock через globrunqputbatch.
При next=true возможны отключения: если !haveSysmon, next принудительно становится false (иначе пара пинг-понгующих горутин могла бы заморить остальных без вытеснения sysmon); при randomizeScheduler с вероятностью 1/2 next сбрасывается в false.
runqsteal/runqgrab: сколько забирают и у кого
runqgrab забирает из очереди жертвы не всю очередь, а её половину с округлением вверх: n := t - h; n = n - n/2. Если n == 0 (очередь пуста), то при stealRunNextG делается попытка забрать единственную горутину из pp.runnext: если она есть и P в состоянии _Prunning, выполняется короткая пауза (usleep(3) или osyield()), чтобы не отнимать G в момент, когда P вот-вот её запустит; затем CAS next→0, и при успехе кража заканчивается, забрав одну горутину. Если runnext пуст или CAS не удался, попытка повторяется, а при отсутствии добычи кража завершается без результата.
При непустой очереди n копируется в batch, а затем атомарно фиксируется через CasRel(&pp.runqhead, h, h+n); при неудаче цикл повторяется. runqsteal вызывает runqgrab в локальную очередь, начиная с t := pp.runqtail; если n == 0 — красть нечего, иначе уменьшает n на 1, берёт последний элемент как возвращаемую G, проверяет переполнение (throw("runqsteal: runq overflow")) и публикует новые элементы через StoreRel(&pp.runqtail, t+n).
Жертва выбирается в stealWork: внешний цикл до stealTries=4, внутренний — по stealOrder.start(cheaprand()), то есть в случайном порядке. Свой P пропускается, а кража не выполняется, если p2 помечен idle.
Цикл schedule: порядок источников работы
schedule начинается с проверки: если M вращается (spinning) и у P есть локальная работа, это ошибка — throw("schedule: spinning with local work"). Затем вызывается findRunnable, который сначала проверяет таймеры P через pp.timers.check(0, nil), потом пробует trace reader и GC worker, затем раз в 61 тик берёт G из глобальной очереди для честности, после чего берёт из локальной очереди через runqget (сначала runnext, затем кольцевой буфер), затем из глобальной батчем, затем опрашивает netpoll, и только потом пытается красть работу через stealWork.
При переходе в spinning M вызывает mp.becomeSpinning(), увеличивая sched.nmspinning; при уходе в сон после releasep() вызывается pidleput(pp, now), увеличивающий sched.npidle. При выходе из spinning в resetspinning уменьшается sched.nmspinning, и отрицательное значение — ошибка (throw("findRunnable: negative nmspinning")). Условие включения spinning: if mp.spinning || 2*sched.nmspinning.Load() < gomaxprocs-sched.npidle.Load() — то есть spinning ограничен половиной занятых P.
Парковка и пробуждение M
M паркуется после того, как перестаёт быть spinning: поток, не нашедший работу в локальной очереди, глобальной очереди или netpoller, считается spinning (состояние в m.spinning и sched.nmspinning); если он не находит работу, он снимает с себя spinning и затем паркуется.
wakep() вызывается при появлении работы и запускает новый поток только если sched.nmspinning равно 0 (сравнение-обмен 0→1), иначе возвращается. Затем берёт idle P через pidlegetSpinning и вызывает startm(pp, true, false). startm при spinning=true требует непустой P (иначе throw), берёт M через mget. Если M нет — резервирует id через mReserveID, разблокирует sched.lock и вызывает newm(fn, pp, id) с fn=mspinning, который ставит m.spinning=true. Если M есть — проверяет, что он не spinning и nextp==0, ставит nmp.spinning=spinning, nmp.nextp.set(pp) и будит через notewakeup(&nmp.park).
nmspinning инкрементируется вызывающим до startm; при переходе spinning→non-spinning поток делает mp.spinning=false и sched.nmspinning.Add(-1), затем с барьером перепроверяет глобальную очередь, локальные очереди, idle-GC и таймеры.
Передача P при handoff реализована в startlockedm: он делает releasep(), mp.nextp.set(pp), notewakeup(&mp.park) и stopm(). stoplockedm при наличии P делает releasep() и handoffp(pp), затем mPark().
note: почему не futex и не каналы
В HACKING.md для одноразовых уведомлений рекомендуется использовать note, предоставляющий notesleep и notewakeup. В отличие от традиционных UNIX sleep/wakeup, note не имеет гонок: notesleep возвращается немедленно, если notewakeup уже произошёл. После использования note можно сбросить через noteclear, но это не должно гоняться со сном или пробуждением.
Блокировка на note блокирует M, но есть два способа сна: notesleep также предотвращает перепланирование связанных G и P, а notetsleepg ведёт себя как блокирующий системный вызов, позволяя P переиспользоваться для другой G; при этом он всё равно менее эффективен, чем прямая блокировка G, поскольку потребляет M.
Для прямого взаимодействия с планировщиком горутин используются gopark и goready: gopark паркует текущую горутину, переводя её в состояние ожидания и убирая из очереди запуска, и планирует другую горутину на текущих M/P; goready возвращает припаркованную горутину в состояние готовности и добавляет её в очередь запуска. По числу блокируемых сущностей варианты различаются: (rw)mutex блокирует G, M и P; note блокирует G и M, а P — в зависимости от варианта; park блокирует только G.
Goroutine: создание, стек, состояния
gopark/goready и sudog
gopark переводит текущую горутину в состояние ожидания: проверяет, что статус G — _Grunning или _Gscanrunning (иначе throw("gopark: bad g status")), сохраняет в M поля waitlock, waitunlockf, waitreason и вызывает mcall(park_m), чтобы переключиться на стек g0. goready через systemstack вызывает ready(gp, traceskip, true). ready проверяет, что статус G равен _Gwaiting (с учётом _Gscan), иначе throw("bad g->status in ready"), затем делает casgstatus(gp, _Gwaiting, _Grunnable), ставит G в очередь runqput и будит P через wakep.
sudog — структура для организации ожидания. acquireSudog берёт её из локального кэша P (pp.sudogcache), при нехватке — из центрального sched.sudogcache, а если пусто — выделяет new(sudog). releaseSudog проверяет, что поля elem, isSelect, next, prev, waitlink, c и gp.param пусты (иначе бросает соответствующую ошибку), и возвращает sudog в кэш P, при переполнении перекладывая половину в центральный кэш.
При парковке с unlockf: gopark сохраняет unlockf в mp.waitunlockf и lock в mp.waitlock, а park_m на стеке g0 вызывает waitunlockf; если unlockf сообщает, что разблокировать не удалось, горутина возобновляется. goparkunlock — обёртка, передающая parkunlock_c, который разблокирует мьютекс и подтверждает, что парковку можно продолжать.
Переходы состояний G и _Gscan-бит
castogscanstatus(gp, oldval, newval) переводит G в состояние со взведённым битом _Gscan (состояние «сканирования», действующее как блокировка). Допустимы только oldval из набора _Grunnable, _Grunning, _Gwaiting, _Gleaked, _Gsyscall, _Gdeadextra и только при условии newval == oldval|_Gscan; тогда выполняется CompareAndSwap, а при успехе acquireLockRankAndM(lockRankGscan). При любом другом oldval/newval печатается диагностика и вызывается throw("bad oldval passed to castogscanstatus").
Обратный переход делает casfrom_Gscanstatus: допустимы oldval из _Gscanrunnable, _Gscanwaiting, _Gscanrunning, _Gscansyscall, _Gscanleaked, _Gscanpreempted, _Gscandeadextra и только при newval == oldval&^_Gscan выполняется CompareAndSwap; иначе — throw. При неудачном CAS — тоже throw.
casgstatus сам не принимает _Gscan-бит: если (oldval&_Gscan != 0) || (newval&_Gscan != 0) || oldval == newval, он печатает и вызывает throw("casgstatus: bad incoming values"), а в цикле CAS ждёт, пока состояние выйдет из scan-состояния, и при oldval == _Gwaiting и текущем _Grunnable бросает throw("casgstatus: waiting for Gwaiting but is Grunnable").
casGToPreemptScan допускает только old == _Grunning и new == _Gscan|_Gpreempted (иначе throw("bad g transition")), берёт lockRankGscan и в цикле CAS переводит _Grunning в _Gscan|_Gpreempted. casGFromPreempted допускает только old == _Gpreempted и new == _Gwaiting, ставит waitreason = waitReasonPreempted; при неудачном CAS переход не происходит, а при успехе уведомляется bubble.changegstatus.
newproc1, malg и кэш gfree
newproc1 создаёт новый G: сначала берёт его из кэша через gfget(pp); если кэш пуст, вызывает malg(stackMin) и переводит G из _Gidle в _Gdead, публикуя через allgadd. malg выделяет стек: при stacksize >= 0 округляет round2(stackSystem + stacksize), вызывает stackalloc и ставит stackguard0 = stack.lo + stackGuard, stackguard1 = ^uintptr(0), обнуляя нижнее слово стека.
Затем newproc1 вычисляет sp = stack.hi - totalSize (где totalSize = alignUp(4*PtrSize + MinFrameSize, StackAlign)), обнуляет sched, ставит sched.sp, sched.pc = goexit+PCQuantum, sched.g и вызывает gostartcallfn.
Наследование: newg.ancestors = saveAncestors(callergp). saveAncestors при debug.tracebackancestors <= 0 или goid==0 ничего не сохраняет, иначе копирует предков вызывающего (кроме корня), обрезая до debug.tracebackancestors, и добавляет запись с pcs/goid/gopc вызывающего. synctest-группы и pprof-метки наследуют только не-системные горутины: newg.bubble = callergp.bubble и newg.labels = mp.curg.labels.
Кэш gfree: gfput кладёт G в pp.gFree, а при size >= 64 переносит партиями по 32 (пока size >= 32) в глобальные sched.gFree.stack/noStack. gfget при пустом локальном кэше забирает из глобального, пока pp.gFree.size < 32, предпочитая G со стеком. gfpurge выгружает все G из pp.gFree в глобальные очереди.
Stop-the-world и преемпция
stopTheWorldWithSema пошагово
Вызывающий уже держит worldsemaсемафор, дающий право пытаться остановить мир и отключил вытеснение. Сама функция помечает текущую горутину как ожидающую приостановки: casGToWaitingForSuspendG(getg().m.curg, _Grunning, waitReasonStoppingTheWorld).
Затем под sched.lock записывается время старта, устанавливается счётчик оставшихся к остановке P равным числу процессоров и флаг ожидания GC: sched.stopwait = gomaxprocs и sched.gcwaiting.Store(true). Текущий P сразу переводится в состояние _Pgcstop (диагностическое состояние остановленного P), его gcStopTime фиксируется, а stopwait уменьшается.
Далее перебираются все P в allp, и те, что находятся в системном вызове, блокируются на выходе из него через setBlockOnExitSyscall, после чего вызываются thread.gcstopP() и thread.resume(). Потом в цикле через pidleget забираются простаивающие P, каждому ставится _Pgcstop, gcStopTime и stopwait--.
Если после этого stopwait > 0, блокировка снимается и вызывается preemptallфункция, которая рассылает всем P запрос на вытеснение текущей горутины, чтобы она дошла до точки, где сможет отдать управление и остановиться. Затем цикл ждёт на sched.stopnote до 100 мкс: if notetsleep(&sched.stopnote, 100*1000) — при сигнале noteclear и выход, иначе повторный preemptall(). В конце проверяются инварианты (stopwait == 0, все P в _Pgcstop, ненулевой gcStopTime), суммируется stoppingCPUTime, вызывается worldStopped() и горутина возвращается в _Grunning.
freezeStopWait — большая константа const freezeStopWait = 0x7fffffff, которую freezetheworld записывает в sched.stopwait, чтобы потребовать постоянной остановки всех G при падении; она выставляется в цикле до 5 раз вместе с gcwaiting и preemptall, поскольку запросы остановки и вытеснения могут теряться из-за гонок.
startTheWorldWithSema и retake
startTheWorldWithSema берёт sched.lock, вычисляет число P (procs := gomaxprocs, либо newprocs), вызывает procresize(procs), который возвращает список P с локальной работой (p1), сбрасывает sched.gcwaiting и, если был установлен sched.sysmonwait, будит sysmon через notewakeup(&sched.sysmonnote).
Затем для каждого P из p1: если у P есть привязанный M (p.m != 0), то P передаётся этому M через mp.nextp.set(p) и M будится notewakeup(&mp.park); иначе запускается новый M через newm(nil, p, -1). После этого фиксируется время STW, вызывается wakep() для пробуждения дополнительного P на случай избытка готовых горутин, и releasem(mp).
Случай P в syscall обрабатывается в retake (sysmon): для P в _Prunning, если schedtick не менялся дольше forcePreemptNS, вызывается preemptone(pp), но горутину в _Gsyscall он не прерывает, поэтому ставится sysretake = true. Далее под sched.lock вызывается setBlockOnExitSyscall(pp), который проверяет, что G в _Gsyscall или _Gdeadextra, ставит _Gscan и подтверждает, что gp.m == mp и gp.m.p == pp; при успехе возвращается syscallingThread.
Если P в syscall дольше одного тика sysmon (и не sysretake), тик обновляется и thread.resume() отпускает G; иначе при непустой очереди или при наличии спиннеров/простаивающих M и недавнем syscall — тоже resume. В противном случае вызывается thread.takeP(), который через releaseP(_Pidle) отвязывает P (pp.m = 0, mp.p = 0, status = _Pidle, addGSyscallNoP, syscalltick++), затем thread.resume(), n++ и handoffp(pp) передаёт P другому потоку.
В exitsyscallTryGetP поток, выходящий из syscall, сам пытается вернуть свой старый P: setBlockOnExitSyscall(oldp), при успехе thread.takeP(), decGSyscallNoP и thread.resume(), возвращая oldp; иначе берёт простаивающий P через pidleget(0) и при sched.sysmonwait будит sysmon.
Sysmon и ретайк P
sysmon — фоновый поток, который всегда работает без P (поэтому в нём запрещены write barriers). Начальная задержка сна — 20 мкс. После 50 циклов без пробуждения задержка удваивается, но ограничена сверху 10 мс. Глубокий сон не наступает, если включён schedtrace или есть активные P: if debug.schedtrace <= 0 && (sched.gcwaiting.Load() || sched.npidle.Load() == gomaxprocs); при пробуждении из syscall idle и delay сбрасываются.
retake перебирает allp, пропуская P не в состоянии _Prunning, и для долго работающего G (schedtick не менялся дольше forcePreemptNS) вызывает preemptone, а если P в syscall — ставит sysretake = true. Для P в syscall retake вызывает setBlockOnExitSyscall, чтобы не дать G выйти из exitsyscall, затем при необходимости забирает P у потока, возобновляет его и передаёт P другому потоку через handoffp(pp). Горутину в системном вызове прерывать бессмысленно: она не в Go-коде, поэтому preemptone для G в _Gsyscall не пытается её вытеснить.
Syscall и cgo: вход и выход
При входе в системный вызов entersyscall вызывает reentersyscall, который сначала увеличивает gp.m.locks, чтобы запретить вытеснение, пока G находится в состоянии Gsyscall с несогласованным g->sched. Затем он копирует syscalltick P в gp.m.syscalltick, сохраняет старый P в gp.m.oldp и запоминает SP/PC/BP вызова в gp.syscallsp/syscallpc/syscallbp для GC и трассировки.
После этого G переводится из _Grunning в _Gsyscall через атомарный CAS (или casgstatus при наличии bubble). P при этом не отбирается сразу — он остаётся у M, но после перехода в _Gsyscall его нельзя трогать: в этот момент P может быть отобран. В отличие от этого, entersyscallblock заранее отдаёт P через handoffp(releasep()) и вызывает addGSyscallNoP, увеличивающий sched.nGsyscallNoP.
При выходе exitsyscall сначала переводит G обратно в _Grunning, забирает и очищает oldp, затем смотрит на gp.m.p: если P ещё есть — это быстрый путь, иначе вызывается exitsyscallTryGetP(oldp), который пытается вернуть старый P через setBlockOnExitSyscall/takeP или взять свободный P из sched.pidle через pidleget(0), уменьшая счётчик через decGSyscallNoP. Если P получить не удалось, вызывается mcall(exitsyscallNoP), который переводит G в _Grunnable, делает dropg, кладёт G в глобальную очередь globrunqput (или при наличии свободного P сразу acquirep и execute), а при заблокированной G — stoplockedm.
Extra M для cgo
Механизм extra M обслуживает cgo-колбэки, приходящие на поток, не созданный Go. needm вызывается, когда колбэк случается на потоке без m, и должен найти m и вернуть его с корректно инициализированными m и g.
Для этого поддерживается стек доступных m, из которого поток «крадёт» m через compare-and-swap с заменой головы на MLOCKED (1) как спин-замок. Инвариант — в списке всегда на один m больше, чем нужно, и в начале программы (если используется cgo) список засевается одним m. getExtraM забирает m из списка и сообщает, что список опустел; needm сохраняет это в mp.needextram, чтобы позже cgocallbackg выделил новый m, а сам needm ставит g0, настраивает границы стека через callbackUpdateSystemStack, вызывает asminit/minit и переводит mp.curg из _Gdeadextra в _Gsyscall.
Если список пуст, lockextra(false) не отдаёт m сразу, а ждёт: увеличивает extraMWaiters и вызывает usleep_no_g(1), пока не появится m. newextram забирает счётчик ожидающих через extraMWaiters.Swap(0) и для каждого вызывает oneNewExtraM, а если ожидающих нет и extraMLength равен 0 — создаёт хотя бы один запасной m. oneNewExtraM выделяет m через allocm(nil, nil, -1) и g через malg(4096), связывает их (gp.m = mp, mp.curg = gp, mp.isextra = true, mp.isExtraInC = true, lockedg/lockedm), ставит g в allg и добавляет m в список через addExtraM.
dropm возвращает m в список: на платформах без pthreads (например, Windows) он вызывается по завершении колбэка, а на pthread-платформах — при выходе не-Go потока через деструктор pthread-ключа; перед возвратом он переводит mp.curg обратно в _Gdeadextra, сбрасывает трассировку, обнуляет границы стека g0 и вызывает putExtraM. cgoBindM привязывает g0 текущего m к потоку через pthread per-thread переменную: проверяет, что текущий g — это g0, и вызывает _cgo_bindm через asmcgocall, чтобы деструктор pthread-ключа смог вызвать dropm при выходе C-потока; needAndBindM вызывает needm(false) и затем cgoBindM, если pthread-ключ создан.
Где смотреть в коде
- proc.go: Store
- proc.go: runqput
- proc.go: castogscanstatus
- proc.go: gcd
- proc.go: set
- proc.go: casGToPreemptScan
- proc.go: ptr
- proc.go: retake