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 )所示。

go_segmented_stacks.jpg

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 。

go_contiguous_stacks.jpg

Figure 2: Contiguous Stacks

Contiguous stacks 也有不足:如果栈已经增加比较大了,可能很难从堆中找到一个更大的可用空间。Contiguous stacks 的设计文档参见 Contiguous stacks

Author: cig01

Created: <2018-11-06 Tue>

Last updated: <2020-07-24 Fri>

Creator: Emacs 27.1 (Org mode 9.4)