【翻译】为何 goroutine 的栈内存无穷大?

一些 Go 语言的新学习者老是会对 goroutine 栈内存占用大小感到很是好奇。这通常是因为程序员进行无限的函数循环调用致使的。为了说明这个问题,请思考如下代码示例(为使问题更加清晰而使用相对刻意的写法): 程序员

package main

import "fmt"

type S struct {
        a, b int
}

// String 实现了接口 fmt.Stringer
func (s *S) String() string {
        return fmt.Sprintf("%s", s) // 调用 Sprintf 时会默认调用 s.String()
}

func main() {
        s := &S{a: 1, b: 2}
        fmt.Println(s)
}

尽管我不建议你这样作,但当你尝试运行这段代码的时候,你会发现你的机器正在进行大量的运算,甚至变得无响应而使你不得不使用 ctrl + c 来中断执行,以避免程序最终达到无药可救的地步;由于我知道你会这样作,因此我为你作好了这一步,你能够直接在 playground 执行这段代码。 golang

许多程序员都曾经写过相似的代码而致使函数的无限循环调用,并使得他们的程序崩溃,但通常状况下并不足以对他们的机器形成毁灭性破坏。问题是,为何 Go 的程序就特殊一点的呢? 安全

goroutine 的一个主要特性就是它们的消耗;建立它们的初始内存成本很低廉(与须要 1 至 8MB 内存的传统 POSIX 线程造成鲜明对比)以及根据须要动态增加和缩减占用的资源。这使得 goroutine 会从 4096 字节的初始栈内存占用开始按需增加或缩减内存占用,而无需担忧资源的耗尽。 架构

为了实现这个目标,连接器(5l、6l 和 8l)会在每一个函数前插入一个序文,这个序文会在函数被调用以前检查判断当前的资源是否知足调用该函数的需求(备注 1)。若是不知足,则调用 runtime.morestack 来分配新的栈页面(备注 2),从函数的调用者那里拷贝函数的参数,而后将控制权返回给调用者。此时,已经能够安全地调用该函数了。当函数执行完毕,事情并无就此结束,函数的返回参数又被拷贝至调用者的栈结构中,而后释放无用的栈空间。 函数

经过这个过程,有效地实现了栈内存的无限使用。假设你并非不断地在两个栈之间往返,通俗地讲叫栈分割,则代价是十分低廉的。 性能

可是我一直注意到一个问题,当你的程序存在函数的无限循环调用而即将致使你的操做系统内存枯竭,而此时又刚好须要分配新的栈页面,则会从堆中分配内存。 学习

当你的函数无止尽地调用着本身,新的栈页面会不断地从堆中分配,继而使得函数又可以继续调用本身。我相信这很快就会使程序用光你机器全部空余的物理内存,交换存储器也会被大量使用,最终致使你的系统变得很是不稳定。 google

能够被 Go 使用的堆内存取决于许多方面,包括你的 CPU 架构以及操做系统,但通常依赖于你机器可用的物理内存,所以你的机器会在即将使用完堆内存以前进行大量交换存储器的操做。 spa

对于 Go 1.1,许多人都但愿能够提高 32 位以及 64 位平台上堆内存使用的最大限制,这个问题会在某些状况下变得更加严重。好比说,你的机器不太可能拥有 128GB 的物理内存(备注 3)。 操作系统

最后要说的是,这里有一些 issue 已经涉及到这个问题(issue1issue2),但仍未找到在不损失性能的状况下可以处理该问题的一个好的解决方案。

备注:
1. 一样适用于方法,但方法的接收者本质上就是函数的第一个参数,当讨论有关 Go 的分段栈的问题时,没有必要将它们区别对待。
2. 使用页面这个词不表明每次分配的内存额度是固定的 4096 字节,必要时会调用 runtime.morestack 来进行新的分配,但我猜想会与页面值的倍数相接近。
3. 因为 Go 1.1 的改动,64 位 Windows 平台的堆内存被限制在 32GB 以内。

原文地址:http://dave.cheney.net/2013/06/02/why-is-a-goroutines-stack-infinite

相关文章
相关标签/搜索