В Go очень интересно устроен стек. Я раньше не особо интересовался деталями реализации, лишь знал, что стек динамически растёт при необходимости, и можно относительно спокойно использовать рекурсию. На практике это пофиг обычно, как именно всё работает, но тут че-то решил загуглить, стало интересно.
Ведь Go компилируется в машинный код (не байт-код), а процессор при исполнении кода умеет только тупо выполнять инструкции по порядку - он не знает про горутины, и что у них надо что-то там увеличивать. Как это всё работает?
Короче.
Если у нормальных потоков ОС стек - это отдельный сегмент памяти фиксированного размера, то в Go стек горутин лежит в куче.
При создании горутины под стек выделяется 2кб, а потом растет по мере необходимости.
Как я уже говорил, процессор ничего не знает про горутины и псевдостеки, он тупо отрабатывает одну инструкцию за другой, а на стек у него стандартно указывает регистр SP (stack pointer)
Поэтому в скомпилированный код перед каждым вызовом любой функции компилятор Go вставляет проверку, а хватит ли в горутиновом стеке места для вызова этой функции. Если нет, то в куче выделяется новое место в 2 раза больше, копируется всё, обновляются указатели, обновляется регистр проца, указывающий на стек. Макс размер стека - 1Гб.
Ну и при переключении горутин, помимо прочего контекста, переключается и SP.
Сама проверка очень легковесная, практически простое сравнение пары чисел. А вот расширение стека, конечно, намного дороже.
Стек умеет не только расти, но и сжиматься. Если горутина перестаёт использовать большую часть своего стека, то при следующем garbage collection стек может быть "обрезан" до меньшего размера, чтобы освободить память.
В итоге:
Переключение между горутинами происходит быстро (выставляются сохраненные регистры, включая SP и указатель на следующую выполняемую инструкцию + еще пару вещей)
Сложно столкнуться с переполнением (stack overflow).
Горутины экономно жрут память, поэтому их можно делать фиговы тыщи.
🫥 Cross Join
⠀
Post #429
3.3K
- 👍 48
- ❤ 2