Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Время от времени новички в Go натыкаются на любопытное свойство языка, связанное с размером стека, доступным для горутины. Обычно это происходит из-за того, что программист непреднамеренно создает бесконечную рекурсию. Чтобы проиллюстрировать это, рассмотрим следующий (слегка надуманный) пример.
package main
import "fmt"
type S struct {
a, b int
}
// String implements the fmt.Stringer interface
func (s *S) String() string {
return fmt.Sprintf("%s", s) // Sprintf will call s.String()
}
func main() {
s := &S{a: 1, b: 2}
fmt.Println(s)
}
Если бы вы запустили эту программу, а я не рекомендую вам это делать, то вы бы обнаружили, что ваша машина начнет активно использовать подкачку и, вероятно, перестанет отвечать, если вы не нажмете ^C до того, как потеряете управление. Так как первое, что все сделают, это попробуют запустить эту программу в песочнице, я избавил вас от хлопот.
Большинство программистов раньше сталкивались с бесконечной рекурсией, и, хотя это фатально для исполняемой программы, обычно оно не влияет на всю машину. Итак, чем же отличаются программы, написанные на Go?
Одна из ключевых особенностей горутин — их стоимость; их дешево создавать с точки зрения начального объема памяти (в отличие от 1–8 мегабайт для традиционного потока POSIX), а их стек увеличивается и уменьшается по мере необходимости. Это позволяет горутинам начинать с одного стека в 4096 байт (для современного Go уже 2048 — прим. переводчика), который затем увеличивается и уменьшается по мере необходимости без риска когда-либо закончиться.
Для этого, линковщик (5l, 6l, 8l) вставляет небольшую "преамбулу" в начало каждой функции [1], которая проверяет, хватает ли этой функции доступного на текущий момент количества памяти в стеке. В случае, если функция требует больше памяти, чем доступно сейчас, вызывается runtime morestack, которая выделяет новую страницу стека [2], копирует аргументы вызывающей стороны (caller), а затем возвращает управление исходной функции, которая теперь может безопасно выполняться. Когда эта функция завершается, всё происходит наоборот, возвращаемые аргументы копируются обратно в кадр стека вызывающей стороны, а ненужное пространство стека освобождается.
Благодаря этому стек в Go фактически бесконечен, и если предположить, что вы не постоянно пересекаете границу между двумя стеками, то это очень дешево.
Однако есть одна деталь, о которой я до сих пор умалчивал и которая связывает случайное использование рекурсивной функции с серьезной нехваткой памяти у вашей операционной системы, а именно то, что когда Go требуются новые страницы стека, они выделяются из кучи.
Поскольку ваша бесконечная функция продолжает вызывать себя, новые страницы стека выделяются из кучи, что позволяет функции продолжать вызывать себя снова и снова. Довольно быстро размер кучи превысит объем свободной физической памяти на вашем компьютере, и в этот момент подкачка сделает вашу машину непригодной для использования.
Размер кучи, доступной для программ Go, зависит от многих факторов, включая архитектуру вашего процессора и вашей операционной системы, но обычно он превышает объем физической памяти вашей машины, поэтому машина, вероятно, будет активно использовать подкачку до того, как программа исчерпает свою кучу.
В Go 1.1 было сильное желание увеличить максимальный размер кучи как для 32-битных, так и для 64-битных платформ, и это в некоторой степени усугубило проблему, т.к. маловероятно, что у вас будет 128 ГБ [3] физической памяти в вашей системе.
В заключение, существует несколько открытых вопросов (ссылка, ссылка), касающихся этой проблемы, но решение, которое не привело бы к снижению производительности для правильно написанных программ, пока еще не найдено.
Прим. переводчика: вот здесь через полгода после написания оригинала статьи (2013 год) авторы Go добавили ограничение по памяти на размер стека горутины, это 1 GB для 64бит и 250 MB для 32бит.
Примечания
1 Это также относится к методам, но поскольку методы реализованы как функции, где первым аргументом является ресивер метода, то в данном контексте практической разницы между функциями и методами нет.
2 Использование слова «страница» не означает, что возможно только фиксированное выделение размером 4096 байт, при необходимости runtime⋅morestack выделит больший объем, вероятно, округленный до границы страницы.
3 64-разрядные платформы Windows допускают кучу только 32 ГБ из-за позднего изменения цикла выпуска Go 1.1.