Goroutine Stack
Table of Contents
1. 栈简介
每个线程(协程)都需要自己的栈内存,用于保存函数局部变量、寄存器数据等内容。Java/C 程序线程栈大小一般默认为 8M(通过 ulimit -s
可查看),当然在创建线程时也可以定制栈的大小(通过 pthread_attr_setstacksize
实现),运行时栈内存的大小不再变化,如果栈不够使用,程序会报错。注:GCC 有一个 Split Stacks 特性,可以动态地调整栈的大小。
在 Golang 中,Goroutine 的初始栈大小为 2Kb(参见源码 https://golang.org/src/runtime/stack.go 中常量 _StackMin
的值),当栈不够使用时会动态地增加,增加后如果不再使用也可能缩小。
Goroutine 的初始栈大小在历史上也不断调整过:Go 1.2 中: goroutine 初始栈大小从 4Kb 调整为了 8Kb;Go 1.4 中: goroutine 初始栈大小从 8Kb 调整为了 2Kb。
2. Goroutine 栈大小的调整策略
当栈不够使用时,需要增加栈大小,下面介绍两种“栈大小的调整策略”。
2.1. Segmented stacks(已放弃)
在 Go 1.2 及之前版本中,采用的是“Segmented stacks”方式来调整栈的大小。
这种方式中,当栈不够使用时,从堆里分配一块新的栈内存(它和当前的栈内存不连续),用完就释放掉,如图 1 (摘自:https://dave.cheney.net/2014/06/07/five-things-that-make-go-fast )所示。
Figure 1: Segmented Stacks
图 1 中,当 G 调用 H 的时候,栈空间不够,这时 Go 运行环境就会从堆里分配一个新的栈内存块去让 H 运行。在 H 返回到 G 后,新分配的内存块马上被释放。一般情况下,“Segmented stacks”工作地非常好,但对有些代码,特别是递归调用,它会造成程序不停地分配和释放新的内存空间,如下面代码:
func G(items []string) { for item := range itmes { H(item) } }
栈不够使用时,G 调用 H 很多次,每调用一次都会导致一次内存的分配和释放。这个问题被称为“hot split problem”,它是 Segmented stacks 的最主要缺点。
由于有 “hot split problem”,Rust 中也放弃了 Segmented stacks,参见 Abandoning segmented stacks in Rust 。
2.2. Stack copying(也称为 Contiguous stacks,目前采用)
由于 Segmented stacks 存在“hot split problem”,从 Go 1.3 起采用的是 Stack copying(也称为 Contiguous stacks)。这种策略下,栈不够使用时,将在堆中分配一个 2 倍大小的内存作为新栈,旧栈的内容会复制到新栈中,然后释放掉。如图 2 (摘自:https://dave.cheney.net/2014/06/07/five-things-that-make-go-fast )所示。不仅有增大过程,还有缩小过程:在非运行中协程,如果栈的使用不超过 1/4 的,栈大小会缩容为原来 1/2 。
Figure 2: Contiguous Stacks
Contiguous stacks 也有不足:如果栈已经增加比较大了,可能很难从堆中找到一个更大的可用空间。Contiguous stacks 的设计文档参见 Contiguous stacks 。