[{"data":1,"prerenderedAt":1392},["ShallowReactive",2],{"content-\u002F03-concurrency\u002F04-scheduler":3},{"id":4,"title":5,"body":6,"description":16,"difficulty":1375,"extension":1376,"meta":1377,"module":1378,"navigation":126,"next":1379,"order":137,"path":1380,"prev":1381,"seo":1382,"slug":1383,"stem":1384,"tags":1385,"__hash__":1391},"content\u002F03-concurrency\u002F04-scheduler\u002Findex.md","Планировщик Go",{"type":7,"value":8,"toc":1360},"minimark",[9,13,17,20,25,28,36,38,42,45,51,57,68,71,81,84,86,89,94,190,196,214,237,239,243,254,260,267,270,283,285,289,296,302,305,307,311,314,317,323,337,340,345,352,358,361,363,367,374,425,436,478,485,487,491,498,504,511,513,517,520,622,632,676,678,682,685,691,740,757,823,836,838,842,853,861,869,877,885,896,904,912,923,925,929,932,934,939,945,951,957,1014,1029,1034,1040,1042,1047,1052,1057,1062,1193,1197,1203,1205,1210,1215,1220,1225,1337,1340,1347,1350,1356],[10,11,5],"h1",{"id":12},"планировщик-go",[14,15,16],"p",{},"К этому моменту мы умеем запускать горутины, синхронизировать их через каналы и sync-примитивы, управлять отменой через контекст. Осталось понять как всё это работает внутри: кто решает какая горутина запустится следующей, как сотни тысяч горутин умещаются на нескольких OS-потоках и почему горутины не блокируют друг друга при системных вызовах. Это и есть планировщик Go.",[18,19],"hr",{},[21,22,24],"h2",{"id":23},"проблема-которую-решает-планировщик","Проблема, которую решает планировщик",[14,26,27],{},"OS-потоки дороги: каждый занимает 1–8 МБ стека, создание требует syscall, переключение контекста — работа ядра. Если запустить 100 000 потоков — система встанет.",[14,29,30,31,35],{},"Go решает это через ",[32,33,34],"strong",{},"мультиплексирование",": тысячи горутин исполняются на небольшом фиксированном пуле OS-потоков. Планировщик Go работает в userspace и сам решает, какую горутину на каком потоке запустить — без участия ядра ОС.",[18,37],{},[21,39,41],{"id":40},"модель-gmp","Модель G\u002FM\u002FP",[14,43,44],{},"Планировщик Go построен на трёх сущностях:",[14,46,47,50],{},[32,48,49],{},"G (Goroutine)"," — горутина. Содержит стек, указатель на текущую инструкцию и служебные поля. Это единица работы с точки зрения планировщика.",[14,52,53,56],{},[32,54,55],{},"M (Machine)"," — OS-поток. Реальный поток операционной системы, который выполняет код. M нужен P чтобы исполнять горутины.",[14,58,59,62,63,67],{},[32,60,61],{},"P (Processor)"," — логический процессор. Содержит локальную очередь горутин и необходимые ресурсы для их исполнения. Количество P равно ",[64,65,66],"code",{},"GOMAXPROCS",".",[14,69,70],{},"Связь между ними:",[72,73,78],"pre",{"className":74,"code":76,"language":77},[75],"language-text","G  G  G  G       G  G  G          G  G\n|  |  |  |       |  |  |          |  |\n└──┴──┴──┘       └──┴──┘          └──┘\nLocal Queue P0   Local Queue P1   Local Queue P2\n     │                │                │\n     M0               M1               M2\n     │                │                │\n  OS Thread        OS Thread        OS Thread\n","text",[64,79,76],{"__ignoreMap":80},"",[14,82,83],{},"M исполняет горутины из очереди своего P. Без P — M не может исполнять горутины. Это ключевое ограничение, объясняющее поведение при syscall.",[18,85],{},[21,87,66],{"id":88},"gomaxprocs",[14,90,91,93],{},[64,92,66],{}," определяет количество P — то есть максимальное число горутин, исполняющихся параллельно:",[72,95,99],{"className":96,"code":97,"language":98,"meta":80,"style":80},"language-go shiki shiki-themes github-dark","import \"runtime\"\n\n\u002F\u002F По умолчанию равно числу логических CPU\nfmt.Println(runtime.GOMAXPROCS(0)) \u002F\u002F 0 = только прочитать, не менять\n\n\u002F\u002F Изменить\nruntime.GOMAXPROCS(4)\n","go",[64,100,101,121,128,135,163,168,174],{"__ignoreMap":80},[102,103,106,110,114,118],"span",{"class":104,"line":105},"line",1,[102,107,109],{"class":108},"snl16","import",[102,111,113],{"class":112},"sU2Wk"," \"",[102,115,117],{"class":116},"svObZ","runtime",[102,119,120],{"class":112},"\"\n",[102,122,124],{"class":104,"line":123},2,[102,125,127],{"emptyLinePlaceholder":126},true,"\n",[102,129,131],{"class":104,"line":130},3,[102,132,134],{"class":133},"sAwPA","\u002F\u002F По умолчанию равно числу логических CPU\n",[102,136,138,142,145,148,150,153,157,160],{"class":104,"line":137},4,[102,139,141],{"class":140},"s95oV","fmt.",[102,143,144],{"class":116},"Println",[102,146,147],{"class":140},"(runtime.",[102,149,66],{"class":116},[102,151,152],{"class":140},"(",[102,154,156],{"class":155},"sDLfK","0",[102,158,159],{"class":140},")) ",[102,161,162],{"class":133},"\u002F\u002F 0 = только прочитать, не менять\n",[102,164,166],{"class":104,"line":165},5,[102,167,127],{"emptyLinePlaceholder":126},[102,169,171],{"class":104,"line":170},6,[102,172,173],{"class":133},"\u002F\u002F Изменить\n",[102,175,177,180,182,184,187],{"class":104,"line":176},7,[102,178,179],{"class":140},"runtime.",[102,181,66],{"class":116},[102,183,152],{"class":140},[102,185,186],{"class":155},"4",[102,188,189],{"class":140},")\n",[14,191,192,193,195],{},"По умолчанию ",[64,194,66],{}," равен числу логических ядер процессора. Это разумный дефолт для CPU-bound задач. Для IO-bound задач (большинство backend-сервисов) значение не имеет принципиального значения — горутины всё равно большую часть времени ждут IO, освобождая P.",[14,197,198,199,201,202,205,206,209,210,213],{},"В контейнерах до Go 1.25 была проблема: Go читал ",[64,200,66],{}," из ",[64,203,204],{},"\u002Fproc\u002Fcpuinfo"," хоста, а не из cgroups контейнера. Контейнер с лимитом в 2 CPU на хосте с 32 ядрами получал ",[64,207,208],{},"GOMAXPROCS=32"," — лишние потоки конкурировали за 2 реальных ядра. Решение — библиотека ",[64,211,212],{},"automaxprocs"," от Uber, которая читает лимиты из cgroups. Начиная с Go 1.25 это поведение исправлено в стандартной библиотеке.",[72,215,217],{"className":96,"code":216,"language":98,"meta":80,"style":80},"import _ \"go.uber.org\u002Fautomaxprocs\" \u002F\u002F автоматически для Go \u003C 1.25\n",[64,218,219],{"__ignoreMap":80},[102,220,221,223,226,229,232,234],{"class":104,"line":105},[102,222,109],{"class":108},[102,224,225],{"class":140}," _ ",[102,227,228],{"class":112},"\"",[102,230,231],{"class":116},"go.uber.org\u002Fautomaxprocs",[102,233,228],{"class":112},[102,235,236],{"class":133}," \u002F\u002F автоматически для Go \u003C 1.25\n",[18,238],{},[21,240,242],{"id":241},"глобальная-очередь-и-локальные-очереди","Глобальная очередь и локальные очереди",[14,244,245,246,249,250,253],{},"У каждого P есть ",[32,247,248],{},"локальная очередь"," горутин (до 256 элементов). Кроме этого существует одна ",[32,251,252],{},"глобальная очередь"," для всего рантайма:",[72,255,258],{"className":256,"code":257,"language":77},[75],"Global Queue: [G] [G] [G] [G] ...\n\nP0 Local: [G] [G] [G]    P1 Local: [G] [G]    P2 Local: []\n",[64,259,257],{"__ignoreMap":80},[14,261,262,263,266],{},"Когда горутина создаётся через ",[64,264,265],{},"go func()",", планировщик сначала пытается положить её в локальную очередь текущего P. Если локальная очередь переполнена — половина горутин переносится в глобальную очередь.",[14,268,269],{},"M берёт горутины для исполнения в следующем порядке:",[271,272,273,277,280],"ol",{},[274,275,276],"li",{},"Из локальной очереди своего P",[274,278,279],{},"Из глобальной очереди (раз в ~61 такт, чтобы глобальные горутины не голодали)",[274,281,282],{},"Через work stealing у других P",[18,284],{},[21,286,288],{"id":287},"work-stealing","Work Stealing",[14,290,291,292,295],{},"Если локальная очередь P пуста — M не простаивает, а ",[32,293,294],{},"крадёт"," горутины у других P:",[72,297,300],{"className":298,"code":299,"language":77},[75],"P0 Local: []  ← пусто        P1 Local: [G1] [G2] [G3] [G4]\n\nP0 крадёт половину у P1 →\n\nP0 Local: [G3] [G4]           P1 Local: [G1] [G2]\n",[64,301,299],{"__ignoreMap":80},[14,303,304],{},"M с пустым P крадёт ровно половину горутин из случайно выбранного P. Это обеспечивает равномерное распределение нагрузки без централизованной координации — каждый P самостоятельно балансирует нагрузку.",[18,306],{},[21,308,310],{"id":309},"что-происходит-при-системном-вызове","Что происходит при системном вызове",[14,312,313],{},"Здесь планировщик делает нечто умное. Когда горутина выполняет блокирующий syscall (например, чтение файла) — она блокирует весь M, потому что syscall выполняется на уровне OS-потока.",[14,315,316],{},"Если бы P остался привязан к этому M — никакие другие горутины не могли бы исполняться. Планировщик решает это так:",[72,318,321],{"className":319,"code":320,"language":77},[75],"До syscall:                     Во время syscall:\n\nP ── M0 ── G (syscall)          M0 ── G (syscall) ← P отвязан\n│                               \n└── очередь горутин             P ── M1 (новый или из пула)\n                                │\n                                └── очередь горутин (продолжают работу)\n",[64,322,320],{"__ignoreMap":80},[271,324,325,328,331,334],{},[274,326,327],{},"Перед syscall P отвязывается от M0",[274,329,330],{},"M1 (свободный поток из пула или новый) подхватывает P",[274,332,333],{},"Остальные горутины продолжают исполняться на M1",[274,335,336],{},"Когда syscall завершается — M0 пытается получить P обратно. Если P свободен — забирает. Если нет — G кладётся в глобальную очередь, M0 уходит в пул",[14,338,339],{},"Именно поэтому Go может эффективно обрабатывать тысячи конкурентных IO-операций: каждая горутина блокируется только на своём syscall, не мешая остальным.",[341,342,344],"h3",{"id":343},"сетевой-io-особый-случай","Сетевой IO — особый случай",[14,346,347,348,351],{},"Сетевой IO в Go обрабатывается иначе через ",[32,349,350],{},"netpoller"," — асинхронный механизм на основе epoll (Linux), kqueue (macOS) или IOCP (Windows):",[72,353,356],{"className":354,"code":355,"language":77},[75],"Горутина делает net.Read()\n       │\n       ├── данные готовы → читаем сразу, горутина не блокируется\n       │\n       └── данных нет → горутина паркуется (не блокирует M!)\n                        netpoller ждёт данных через epoll\n                        когда данные пришли → горутина возвращается в очередь\n",[64,357,355],{"__ignoreMap":80},[14,359,360],{},"Сетевая горутина паркуется без блокировки M — поэтому тысячи горутин с открытыми TCP-соединениями не создают тысячи OS-потоков. M освобождается и исполняет другие горутины.",[18,362],{},[21,364,366],{"id":365},"preemption-вытеснение-горутин","Preemption — вытеснение горутин",[14,368,369,370,373],{},"Ранние версии Go (до 1.14) использовали ",[32,371,372],{},"кооперативное вытеснение",": горутина могла быть вытеснена только в точках, где она явно передавала управление — при вызове функций, операциях с каналами, syscall. Горутина в плотном цикле без вызовов функций могла монополизировать M:",[72,375,377],{"className":96,"code":376,"language":98,"meta":80,"style":80},"\u002F\u002F До Go 1.14: этот цикл держал M и не давал другим горутинам работать\ngo func() {\n    for {\n        \u002F\u002F плотный цикл без function calls — планировщик не может вытеснить\n        i++\n    }\n}()\n",[64,378,379,384,394,402,407,415,420],{"__ignoreMap":80},[102,380,381],{"class":104,"line":105},[102,382,383],{"class":133},"\u002F\u002F До Go 1.14: этот цикл держал M и не давал другим горутинам работать\n",[102,385,386,388,391],{"class":104,"line":123},[102,387,98],{"class":108},[102,389,390],{"class":108}," func",[102,392,393],{"class":140},"() {\n",[102,395,396,399],{"class":104,"line":130},[102,397,398],{"class":108},"    for",[102,400,401],{"class":140}," {\n",[102,403,404],{"class":104,"line":137},[102,405,406],{"class":133},"        \u002F\u002F плотный цикл без function calls — планировщик не может вытеснить\n",[102,408,409,412],{"class":104,"line":165},[102,410,411],{"class":140},"        i",[102,413,414],{"class":108},"++\n",[102,416,417],{"class":104,"line":170},[102,418,419],{"class":140},"    }\n",[102,421,422],{"class":104,"line":176},[102,423,424],{"class":140},"}()\n",[14,426,427,428,431,432,435],{},"С Go 1.14 введено ",[32,429,430],{},"асинхронное вытеснение"," (asynchronous preemption): планировщик посылает горутине сигнал ",[64,433,434],{},"SIGURG"," (на Unix), который прерывает исполнение в любой точке кода — даже в середине плотного цикла. Горутина сохраняет состояние и помещается обратно в очередь:",[72,437,439],{"className":96,"code":438,"language":98,"meta":80,"style":80},"\u002F\u002F Go 1.14+: этот цикл корректно вытесняется планировщиком\ngo func() {\n    for {\n        i++ \u002F\u002F SIGURG может прийти в любой момент\n    }\n}()\n",[64,440,441,446,454,460,470,474],{"__ignoreMap":80},[102,442,443],{"class":104,"line":105},[102,444,445],{"class":133},"\u002F\u002F Go 1.14+: этот цикл корректно вытесняется планировщиком\n",[102,447,448,450,452],{"class":104,"line":123},[102,449,98],{"class":108},[102,451,390],{"class":108},[102,453,393],{"class":140},[102,455,456,458],{"class":104,"line":130},[102,457,398],{"class":108},[102,459,401],{"class":140},[102,461,462,464,467],{"class":104,"line":137},[102,463,411],{"class":140},[102,465,466],{"class":108},"++",[102,468,469],{"class":133}," \u002F\u002F SIGURG может прийти в любой момент\n",[102,471,472],{"class":104,"line":165},[102,473,419],{"class":140},[102,475,476],{"class":104,"line":170},[102,477,424],{"class":140},[14,479,480,481,484],{},"Вытеснение происходит примерно каждые ",[32,482,483],{},"10 мс"," — это квант времени Go-планировщика.",[18,486],{},[21,488,490],{"id":489},"spinning-потоки","spinning потоки",[14,492,493,494,497],{},"Когда очереди горутин пусты, M не засыпает немедленно. Несколько M переходят в ",[32,495,496],{},"spinning"," режим — активно опрашивают очереди в ожидании новых горутин:",[72,499,502],{"className":500,"code":501,"language":77},[75],"M0: исполняет G\nM1: spinning ← активно проверяет очереди\nM2: sleeping  ← припаркован, ждёт сигнала\n",[64,503,501],{"__ignoreMap":80},[14,505,506,507,510],{},"Spinning позволяет мгновенно подхватить новую горутину без latency на пробуждение потока (которое занимает микросекунды). Количество spinning потоков ограничено — не более ",[64,508,509],{},"GOMAXPROCS\u002F2",", чтобы не жечь CPU впустую.",[18,512],{},[21,514,516],{"id":515},"runtimegosched-и-ручное-управление","runtime.Gosched и ручное управление",[14,518,519],{},"В редких случаях горутина может явно уступить управление:",[72,521,523],{"className":96,"code":522,"language":98,"meta":80,"style":80},"func heavyComputation() {\n    for i := 0; i \u003C 1_000_000_000; i++ {\n        doWork(i)\n        if i%10_000 == 0 {\n            runtime.Gosched() \u002F\u002F явно уступаем — даём другим горутинам поработать\n        }\n    }\n}\n",[64,524,525,535,564,572,593,607,612,616],{"__ignoreMap":80},[102,526,527,530,533],{"class":104,"line":105},[102,528,529],{"class":108},"func",[102,531,532],{"class":116}," heavyComputation",[102,534,393],{"class":140},[102,536,537,539,542,545,548,551,554,557,560,562],{"class":104,"line":123},[102,538,398],{"class":108},[102,540,541],{"class":140}," i ",[102,543,544],{"class":108},":=",[102,546,547],{"class":155}," 0",[102,549,550],{"class":140},"; i ",[102,552,553],{"class":108},"\u003C",[102,555,556],{"class":155}," 1_000_000_000",[102,558,559],{"class":140},"; i",[102,561,466],{"class":108},[102,563,401],{"class":140},[102,565,566,569],{"class":104,"line":130},[102,567,568],{"class":116},"        doWork",[102,570,571],{"class":140},"(i)\n",[102,573,574,577,580,583,586,589,591],{"class":104,"line":137},[102,575,576],{"class":108},"        if",[102,578,579],{"class":140}," i",[102,581,582],{"class":108},"%",[102,584,585],{"class":155},"10_000",[102,587,588],{"class":108}," ==",[102,590,547],{"class":155},[102,592,401],{"class":140},[102,594,595,598,601,604],{"class":104,"line":165},[102,596,597],{"class":140},"            runtime.",[102,599,600],{"class":116},"Gosched",[102,602,603],{"class":140},"() ",[102,605,606],{"class":133},"\u002F\u002F явно уступаем — даём другим горутинам поработать\n",[102,608,609],{"class":104,"line":170},[102,610,611],{"class":140},"        }\n",[102,613,614],{"class":104,"line":176},[102,615,419],{"class":140},[102,617,619],{"class":104,"line":618},8,[102,620,621],{"class":140},"}\n",[14,623,624,627,628,631],{},[64,625,626],{},"runtime.Gosched()"," — аналог ",[64,629,630],{},"yield"," в других языках. Горутина помещается в конец очереди, планировщик выбирает следующую. С Go 1.14 это редко нужно — асинхронное вытеснение справляется само.",[72,633,635],{"className":96,"code":634,"language":98,"meta":80,"style":80},"\u002F\u002F Другие полезные функции\nruntime.NumGoroutine()  \u002F\u002F текущее количество горутин\nruntime.NumCPU()        \u002F\u002F число логических CPU\nruntime.GOOS            \u002F\u002F операционная система (константа)\n",[64,636,637,642,655,668],{"__ignoreMap":80},[102,638,639],{"class":104,"line":105},[102,640,641],{"class":133},"\u002F\u002F Другие полезные функции\n",[102,643,644,646,649,652],{"class":104,"line":123},[102,645,179],{"class":140},[102,647,648],{"class":116},"NumGoroutine",[102,650,651],{"class":140},"()  ",[102,653,654],{"class":133},"\u002F\u002F текущее количество горутин\n",[102,656,657,659,662,665],{"class":104,"line":130},[102,658,179],{"class":140},[102,660,661],{"class":116},"NumCPU",[102,663,664],{"class":140},"()        ",[102,666,667],{"class":133},"\u002F\u002F число логических CPU\n",[102,669,670,673],{"class":104,"line":137},[102,671,672],{"class":140},"runtime.GOOS            ",[102,674,675],{"class":133},"\u002F\u002F операционная система (константа)\n",[18,677],{},[21,679,681],{"id":680},"как-планировщик-влияет-на-код","Как планировщик влияет на код",[14,683,684],{},"Понимание планировщика объясняет несколько неочевидных вещей:",[14,686,687,690],{},[32,688,689],{},"Порядок исполнения горутин не гарантирован."," Даже если горутина создана раньше — она может исполниться позже. Это зависит от состояния очередей и work stealing:",[72,692,694],{"className":96,"code":693,"language":98,"meta":80,"style":80},"for i := 0; i \u003C 5; i++ {\n    go fmt.Println(i) \u002F\u002F порядок вывода непредсказуем\n}\n",[64,695,696,720,736],{"__ignoreMap":80},[102,697,698,701,703,705,707,709,711,714,716,718],{"class":104,"line":105},[102,699,700],{"class":108},"for",[102,702,541],{"class":140},[102,704,544],{"class":108},[102,706,547],{"class":155},[102,708,550],{"class":140},[102,710,553],{"class":108},[102,712,713],{"class":155}," 5",[102,715,559],{"class":140},[102,717,466],{"class":108},[102,719,401],{"class":140},[102,721,722,725,728,730,733],{"class":104,"line":123},[102,723,724],{"class":108},"    go",[102,726,727],{"class":140}," fmt.",[102,729,144],{"class":116},[102,731,732],{"class":140},"(i) ",[102,734,735],{"class":133},"\u002F\u002F порядок вывода непредсказуем\n",[102,737,738],{"class":104,"line":130},[102,739,621],{"class":140},[14,741,742,748,749,752,753,756],{},[32,743,744,747],{},[64,745,746],{},"runtime.LockOSThread()"," — привязка к потоку."," Некоторые C-библиотеки (OpenGL, GUI) требуют вызовов из одного и того же OS-потока. ",[64,750,751],{},"LockOSThread"," привязывает текущую горутину к текущему M навсегда (пока не вызовет ",[64,754,755],{},"UnlockOSThread","):",[72,758,760],{"className":96,"code":759,"language":98,"meta":80,"style":80},"func glWorker() {\n    runtime.LockOSThread()\n    defer runtime.UnlockOSThread()\n\n    \u002F\u002F все вызовы OpenGL из этой горутины — на одном M\n    gl.Init()\n    renderLoop()\n}\n",[64,761,762,771,781,793,797,802,812,819],{"__ignoreMap":80},[102,763,764,766,769],{"class":104,"line":105},[102,765,529],{"class":108},[102,767,768],{"class":116}," glWorker",[102,770,393],{"class":140},[102,772,773,776,778],{"class":104,"line":123},[102,774,775],{"class":140},"    runtime.",[102,777,751],{"class":116},[102,779,780],{"class":140},"()\n",[102,782,783,786,789,791],{"class":104,"line":130},[102,784,785],{"class":108},"    defer",[102,787,788],{"class":140}," runtime.",[102,790,755],{"class":116},[102,792,780],{"class":140},[102,794,795],{"class":104,"line":137},[102,796,127],{"emptyLinePlaceholder":126},[102,798,799],{"class":104,"line":165},[102,800,801],{"class":133},"    \u002F\u002F все вызовы OpenGL из этой горутины — на одном M\n",[102,803,804,807,810],{"class":104,"line":170},[102,805,806],{"class":140},"    gl.",[102,808,809],{"class":116},"Init",[102,811,780],{"class":140},[102,813,814,817],{"class":104,"line":176},[102,815,816],{"class":116},"    renderLoop",[102,818,780],{"class":140},[102,820,821],{"class":104,"line":618},[102,822,621],{"class":140},[14,824,825,828,829,831,832,835],{},[32,826,827],{},"Горутины на syscall создают дополнительные M."," Если все горутины одновременно делают блокирующие syscall — рантайм создаёт новые M. Их количество не ограничено ",[64,830,66],{}," и может стать очень большим. В экстремальных случаях это приводит к исчерпанию ресурсов. Ограничение через ",[64,833,834],{},"runtime\u002Fdebug.SetMaxThreads"," (по умолчанию 10 000).",[18,837],{},[21,839,841],{"id":840},"вопросы-на-собеседовании","Вопросы на собеседовании",[14,843,844,847,850,851,67],{},[32,845,846],{},"Q: Что такое G, M и P в планировщике Go?",[848,849],"br",{},"\nA: G — горутина, единица работы со своим стеком и контекстом исполнения. M — OS-поток, реальный поток операционной системы. P — логический процессор, содержит локальную очередь горутин и необходимые ресурсы. M исполняет горутины только при наличии P. Количество P определяется ",[64,852,66],{},[14,854,855,858,860],{},[32,856,857],{},"Q: Что такое work stealing?",[848,859],{},"\nA: Механизм балансировки нагрузки: если локальная очередь P пуста, M крадёт половину горутин из случайно выбранного другого P. Это обеспечивает равномерное распределение без централизованной координации и позволяет избежать простоя потоков.",[14,862,863,866,868],{},[32,864,865],{},"Q: Что происходит когда горутина делает блокирующий syscall?",[848,867],{},"\nA: P отвязывается от текущего M. Свободный M (или новый) подхватывает P и продолжает исполнять другие горутины. Когда syscall завершается, M пытается вернуть P. Если P занят — горутина кладётся в глобальную очередь, M уходит в пул.",[14,870,871,874,876],{},[32,872,873],{},"Q: Как Go обрабатывает тысячи сетевых соединений не создавая тысячи потоков?",[848,875],{},"\nA: Через netpoller на основе epoll\u002Fkqueue\u002FIOCP. Горутина, ожидающая сетевого IO, паркуется без блокировки M — поток освобождается и исполняет другие горутины. Когда данные готовы, netpoller возвращает горутину в очередь планировщика.",[14,878,879,882,884],{},[32,880,881],{},"Q: Что такое кооперативное и асинхронное вытеснение? Когда изменилось?",[848,883],{},"\nA: До Go 1.14 — кооперативное: горутина вытесняется только в точках явной передачи управления (вызов функции, канал, syscall). Плотный цикл без вызовов мог монополизировать поток. С Go 1.14 — асинхронное: планировщик посылает SIGURG, прерывающий горутину в любой точке. Квант времени — ~10 мс.",[14,886,887,890,892,893,895],{},[32,888,889],{},"Q: Что такое GOMAXPROCS и как правильно настраивать в контейнере?",[848,891],{},"\nA: Количество P — максимум параллельно исполняемых горутин. По умолчанию равно числу логических CPU. В контейнерах до Go 1.25 читался из хоста, а не из cgroups — решение: ",[64,894,212],{}," от Uber. В Go 1.25 исправлено в стандартной библиотеке.",[14,897,898,901,903],{},[32,899,900],{},"Q: Зачем нужны spinning потоки?",[848,902],{},"\nA: Когда очереди пусты, часть M активно опрашивают очереди вместо немедленного засыпания. Это позволяет мгновенно подхватить новую горутину без latency на пробуждение потока. Количество spinning M ограничено GOMAXPROCS\u002F2.",[14,905,906,909,911],{},[32,907,908],{},"Q: Когда и зачем использовать runtime.LockOSThread?",[848,910],{},"\nA: Когда C-библиотека требует вызовов из одного и того же OS-потока (OpenGL, некоторые GUI-фреймворки, CGo с thread-local state). LockOSThread привязывает горутину к текущему M — планировщик не перенесёт её на другой поток.",[14,913,914,917,919,920,922],{},[32,915,916],{},"Q: Почему количество M не ограничено GOMAXPROCS?",[848,918],{},"\nA: GOMAXPROCS ограничивает только P — активно исполняющие горутины. M создаются под каждый блокирующий syscall чтобы P мог продолжать работу с другим M. В теории M может быть намного больше GOMAXPROCS если много горутин одновременно в syscall. Лимит — ",[64,921,834],{},", по умолчанию 10 000.",[18,924],{},[10,926,928],{"id":927},"задачи-планировщик","Задачи: Планировщик",[14,930,931],{},"Задачи по планировщику — концептуальные. Здесь важно объяснение, а не код.",[18,933],{},[14,935,936],{},[32,937,938],{},"Задача 1: Сколько реально параллельно",[14,940,941,944],{},[32,942,943],{},"Уровень:"," Лёгкая",[14,946,947,950],{},[32,948,949],{},"Что проверяет:"," понимание GOMAXPROCS и разницы между конкурентностью и параллелизмом",[14,952,953,956],{},[32,954,955],{},"Условие:"," Ответь на вопросы и объясни:",[72,958,960],{"className":96,"code":959,"language":98,"meta":80,"style":80},"runtime.GOMAXPROCS(2)\n\nfor i := 0; i \u003C 10; i++ {\n    go heavyComputation()\n}\n",[64,961,962,975,979,1002,1010],{"__ignoreMap":80},[102,963,964,966,968,970,973],{"class":104,"line":105},[102,965,179],{"class":140},[102,967,66],{"class":116},[102,969,152],{"class":140},[102,971,972],{"class":155},"2",[102,974,189],{"class":140},[102,976,977],{"class":104,"line":123},[102,978,127],{"emptyLinePlaceholder":126},[102,980,981,983,985,987,989,991,993,996,998,1000],{"class":104,"line":130},[102,982,700],{"class":108},[102,984,541],{"class":140},[102,986,544],{"class":108},[102,988,547],{"class":155},[102,990,550],{"class":140},[102,992,553],{"class":108},[102,994,995],{"class":155}," 10",[102,997,559],{"class":140},[102,999,466],{"class":108},[102,1001,401],{"class":140},[102,1003,1004,1006,1008],{"class":104,"line":137},[102,1005,724],{"class":108},[102,1007,532],{"class":116},[102,1009,780],{"class":140},[102,1011,1012],{"class":104,"line":165},[102,1013,621],{"class":140},[271,1015,1016,1019,1022],{},[274,1017,1018],{},"Сколько горутин запущено?",[274,1020,1021],{},"Сколько из них выполняются параллельно в один момент времени?",[274,1023,1024,1025,1028],{},"Что изменится если ",[64,1026,1027],{},"GOMAXPROCS(1)","?",[14,1030,1031],{},[32,1032,1033],{},"Решение:",[72,1035,1038],{"className":1036,"code":1037,"language":77},[75],"1. Запущено 10 горутин.\n\n2. Параллельно выполняются максимум 2 — по числу P (GOMAXPROCS=2).\n   Остальные 8 ждут в локальных очередях P.\n\n3. При GOMAXPROCS(1) — только 1 P, горутины выполняются\n   конкурентно но не параллельно. Планировщик переключает\n   их на одном OS-потоке. Для CPU-bound задач это в 2 раза\n   медленнее чем GOMAXPROCS(2).\n\nВажно: конкурентность (много горутин) ≠ параллелизм\n(одновременное выполнение). Параллелизм ограничен GOMAXPROCS.\n",[64,1039,1037],{"__ignoreMap":80},[18,1041],{},[14,1043,1044],{},[32,1045,1046],{},"Задача 2: Почему не зависает",[14,1048,1049,1051],{},[32,1050,943],{}," Средняя",[14,1053,1054,1056],{},[32,1055,949],{}," понимание планировщика при syscall, netpoller",[14,1058,1059,1061],{},[32,1060,955],{}," Объясни почему этот код не блокирует сервер несмотря на то что каждый запрос делает сетевой вызов:",[72,1063,1065],{"className":96,"code":1064,"language":98,"meta":80,"style":80},"http.HandleFunc(\"\u002F\", func(w http.ResponseWriter, r *http.Request) {\n    resp, _ := http.Get(\"https:\u002F\u002Fapi.example.com\u002Fdata\") \u002F\u002F сетевой вызов\n    defer resp.Body.Close()\n    io.Copy(w, resp.Body)\n})\n\nhttp.ListenAndServe(\":8080\", nil)\n",[64,1066,1067,1118,1142,1154,1165,1170,1174],{"__ignoreMap":80},[102,1068,1069,1072,1075,1077,1080,1083,1085,1087,1091,1094,1096,1099,1101,1104,1107,1110,1112,1115],{"class":104,"line":105},[102,1070,1071],{"class":140},"http.",[102,1073,1074],{"class":116},"HandleFunc",[102,1076,152],{"class":140},[102,1078,1079],{"class":112},"\"\u002F\"",[102,1081,1082],{"class":140},", ",[102,1084,529],{"class":108},[102,1086,152],{"class":140},[102,1088,1090],{"class":1089},"s9osk","w",[102,1092,1093],{"class":116}," http",[102,1095,67],{"class":140},[102,1097,1098],{"class":116},"ResponseWriter",[102,1100,1082],{"class":140},[102,1102,1103],{"class":1089},"r",[102,1105,1106],{"class":108}," *",[102,1108,1109],{"class":116},"http",[102,1111,67],{"class":140},[102,1113,1114],{"class":116},"Request",[102,1116,1117],{"class":140},") {\n",[102,1119,1120,1123,1125,1128,1131,1133,1136,1139],{"class":104,"line":123},[102,1121,1122],{"class":140},"    resp, _ ",[102,1124,544],{"class":108},[102,1126,1127],{"class":140}," http.",[102,1129,1130],{"class":116},"Get",[102,1132,152],{"class":140},[102,1134,1135],{"class":112},"\"https:\u002F\u002Fapi.example.com\u002Fdata\"",[102,1137,1138],{"class":140},") ",[102,1140,1141],{"class":133},"\u002F\u002F сетевой вызов\n",[102,1143,1144,1146,1149,1152],{"class":104,"line":130},[102,1145,785],{"class":108},[102,1147,1148],{"class":140}," resp.Body.",[102,1150,1151],{"class":116},"Close",[102,1153,780],{"class":140},[102,1155,1156,1159,1162],{"class":104,"line":137},[102,1157,1158],{"class":140},"    io.",[102,1160,1161],{"class":116},"Copy",[102,1163,1164],{"class":140},"(w, resp.Body)\n",[102,1166,1167],{"class":104,"line":165},[102,1168,1169],{"class":140},"})\n",[102,1171,1172],{"class":104,"line":170},[102,1173,127],{"emptyLinePlaceholder":126},[102,1175,1176,1178,1181,1183,1186,1188,1191],{"class":104,"line":176},[102,1177,1071],{"class":140},[102,1179,1180],{"class":116},"ListenAndServe",[102,1182,152],{"class":140},[102,1184,1185],{"class":112},"\":8080\"",[102,1187,1082],{"class":140},[102,1189,1190],{"class":155},"nil",[102,1192,189],{"class":140},[14,1194,1195],{},[32,1196,1033],{},[72,1198,1201],{"className":1199,"code":1200,"language":77},[75],"Каждый входящий запрос обрабатывается в отдельной горутине.\nПри вызове http.Get — сетевой IO обрабатывается через netpoller\n(epoll на Linux).\n\nКогда горутина ждёт ответа от api.example.com:\n1. Горутина паркуется — не блокирует свой M (OS-поток)\n2. netpoller регистрирует дескриптор в epoll\n3. M освобождается и берёт другую горутину из очереди\n4. Когда данные пришли — netpoller возвращает горутину в очередь\n\nРезультат: 10 000 одновременных запросов к внешнему API\n= 10 000 припаркованных горутин, но всего GOMAXPROCS активных потоков.\nСервер не блокируется.\n",[64,1202,1200],{"__ignoreMap":80},[18,1204],{},[14,1206,1207],{},[32,1208,1209],{},"Задача 3: Плотный цикл",[14,1211,1212,1214],{},[32,1213,943],{}," Сложная",[14,1216,1217,1219],{},[32,1218,949],{}," понимание preemption, разница до и после Go 1.14",[14,1221,1222,1224],{},[32,1223,955],{}," Что произойдёт при запуске этого кода на Go 1.13 и на Go 1.14+? Объясни разницу.",[72,1226,1228],{"className":96,"code":1227,"language":98,"meta":80,"style":80},"runtime.GOMAXPROCS(1)\n\ngo func() {\n    for {\n        \u002F\u002F плотный цикл без вызовов функций\n        _ = 1 + 1\n    }\n}()\n\ntime.Sleep(time.Second)\nfmt.Println(\"main продолжается\")\\n```\n\n**Решение:**\n\n",[64,1229,1230,1243,1247,1255,1261,1266,1283,1287,1291,1296,1308,1326,1331],{"__ignoreMap":80},[102,1231,1232,1234,1236,1238,1241],{"class":104,"line":105},[102,1233,179],{"class":140},[102,1235,66],{"class":116},[102,1237,152],{"class":140},[102,1239,1240],{"class":155},"1",[102,1242,189],{"class":140},[102,1244,1245],{"class":104,"line":123},[102,1246,127],{"emptyLinePlaceholder":126},[102,1248,1249,1251,1253],{"class":104,"line":130},[102,1250,98],{"class":108},[102,1252,390],{"class":108},[102,1254,393],{"class":140},[102,1256,1257,1259],{"class":104,"line":137},[102,1258,398],{"class":108},[102,1260,401],{"class":140},[102,1262,1263],{"class":104,"line":165},[102,1264,1265],{"class":133},"        \u002F\u002F плотный цикл без вызовов функций\n",[102,1267,1268,1271,1274,1277,1280],{"class":104,"line":170},[102,1269,1270],{"class":140},"        _ ",[102,1272,1273],{"class":108},"=",[102,1275,1276],{"class":155}," 1",[102,1278,1279],{"class":108}," +",[102,1281,1282],{"class":155}," 1\n",[102,1284,1285],{"class":104,"line":176},[102,1286,419],{"class":140},[102,1288,1289],{"class":104,"line":618},[102,1290,424],{"class":140},[102,1292,1294],{"class":104,"line":1293},9,[102,1295,127],{"emptyLinePlaceholder":126},[102,1297,1299,1302,1305],{"class":104,"line":1298},10,[102,1300,1301],{"class":140},"time.",[102,1303,1304],{"class":116},"Sleep",[102,1306,1307],{"class":140},"(time.Second)\n",[102,1309,1311,1313,1315,1317,1320,1323],{"class":104,"line":1310},11,[102,1312,141],{"class":140},[102,1314,144],{"class":116},[102,1316,152],{"class":140},[102,1318,1319],{"class":112},"\"main продолжается\"",[102,1321,1322],{"class":140},")\\n",[102,1324,1325],{"class":112},"```\n",[102,1327,1329],{"class":104,"line":1328},12,[102,1330,127],{"emptyLinePlaceholder":126},[102,1332,1334],{"class":104,"line":1333},13,[102,1335,1336],{"class":112},"**Решение:**\n",[14,1338,1339],{},"Go 1.13 (кооперативное вытеснение):\nГорутина в плотном цикле без вызовов функций НИКОГДА\nне отдаёт управление планировщику. При GOMAXPROCS(1) есть\nтолько один P — он занят бесконечным циклом.\ntime.Sleep в main никогда не сработает — deadlock или\nmain никогда не продолжится.",[14,1341,1342,1343,1346],{},"Go 1.14+ (асинхронное вытеснение):\nПланировщик посылает SIGURG каждые ~10ms.\nГорутина прерывается в любой точке — даже внутри ",[64,1344,1345],{},"_ = 1+1",".\nСохраняется состояние регистров, горутина кладётся в очередь.\nmain продолжается через секунду, выводит \"main продолжается\".",[14,1348,1349],{},"Вывод: до Go 1.14 плотные циклы без function calls\nмогли монополизировать поток. После 1.14 — нет.\nruntime.Gosched() раньше был обязателен в таких циклах.",[72,1351,1354],{"className":1352,"code":1353,"language":77},[75],"\n---\n",[64,1355,1353],{"__ignoreMap":80},[1357,1358,1359],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":80,"searchDepth":123,"depth":123,"links":1361},[1362,1363,1364,1365,1366,1367,1370,1371,1372,1373,1374],{"id":23,"depth":123,"text":24},{"id":40,"depth":123,"text":41},{"id":88,"depth":123,"text":66},{"id":241,"depth":123,"text":242},{"id":287,"depth":123,"text":288},{"id":309,"depth":123,"text":310,"children":1368},[1369],{"id":343,"depth":130,"text":344},{"id":365,"depth":123,"text":366},{"id":489,"depth":123,"text":490},{"id":515,"depth":123,"text":516},{"id":680,"depth":123,"text":681},{"id":840,"depth":123,"text":841},"advanced","md",{},"concurrency","concurrency-patterns","\u002F03-concurrency\u002F04-scheduler","context",{"title":5,"description":16},"scheduler","03-concurrency\u002F04-scheduler\u002Findex",[1386,1387,66,1388,1389,1390],"планировщик","G\u002FM\u002FP","work stealing","preemption","собеседование","l2RKSfnOBBNJC3GbAyxlllCOU5MJTIwaSYPeAeYHVAQ",1776289835680]