为什么Go语言不会出现栈溢出?

20
5个回答

16
这是一个名为“分段栈”的特性:每个goroutine都有自己的堆栈,在堆上分配
在最简单的情况下,编程语言实现使用每个进程/地址空间一个堆栈,通常使用称为pushpop(或类似名称)的特殊处理器指令进行管理,并作为从固定地址开始的堆栈帧的动态数组实现(通常是虚拟内存的顶部)。
那样(或曾经是)很快,但并不特别安全。当大量代码在同一个地址空间(线程)中并发执行时会引起麻烦。现在每个线程都需要自己的堆栈。但是,除了可能只有一个之外,所有堆栈都必须是固定大小,以免它们互相重叠或与堆重叠。
然而,任何使用堆栈的编程语言也可以通过以不同的方式管理堆栈来实现:通过使用保存堆栈帧的列表数据结构或类似的东西,但实际上是在堆上分配的。在堆填满之前不会出现堆栈溢出。

1
每个线程都有自己的堆栈,因此“当大量代码在同一地址空间(线程)中并发执行时会引起麻烦”是不正确的。 - dan_waterworth
11
很多内容是错误的,Go语言仍然使用push/pop指令(实际上他们并没有使用,GCC也没有[mov off(%esp)]),只是它将堆分配的堆栈设置为堆栈和堆栈基址寄存器。 - cthom06
1
@poolie:在C或C++中,堆栈溢出异常是一种痛苦。它迫使您手动创建递归函数的迭代版本,或者像CLang在过去两周中所做的那样将执行移动到单独的线程中...只有在您能够预见到问题时才会这样做。为了规避这个问题,许多人会简单地设置一个大的堆栈(在我工作的地方,每个线程都是8MB),但这仍然涉及调整和猜测。不必担心堆栈大小可以增加安全性(而非安全)。 - Matthieu M.
1
根据这里的评论,这个答案需要更多的解释。分段堆栈不仅仅是堆分配的。Go运行时确保在函数开始时堆栈足够大(参见runtime·morestack),如果不够大,则为堆栈分配更多空间(如果内存不足,则会引发panic)。 - cthom06
2
我理解这个好处,只是我不确定在这种情况下他们所说的“堆栈溢出”是否指的是这个。顺便说一下,在64位机器上,由于有足够的地址空间来容纳间隔较大的堆栈,这个好处有些被稀释了:给它们每个4GB并感到高兴。(显然,并非所有机器都是64位的。) - poolie
显示剩余10条评论

5

它使用分段栈。这基本上意味着它使用链表而不是固定大小的数组作为堆栈。当它用完空间时,会使堆栈变得更大一些。

编辑:

这里有更多信息:http://golang.org/doc/go_faq.html#goroutines

这样做之所以很好,不是因为它永远不会溢出(这是一个好的副作用),而是因为您可以创建具有非常小的内存占用的线程,这意味着您可以拥有很多线程。


2

我认为他们不能完全避免堆栈溢出。他们提供了一种防止最典型的与编程有关的错误导致堆栈溢出的方法。

当内存用尽时,就没有办法防止堆栈溢出。


2
但是,任何编写会导致堆栈溢出的程序的人都是错误的。 - dan_waterworth
1
这就是大多数人的情况,这也是Go语言设计者试图避免这种情况的原因。 - fabrizioM
1
90%的人会在基于堆的栈(例如Go语言的栈)上溢出。 - dan_waterworth
2
你不能真正地,根据定义,在基于堆的分段栈上溢出。堆增长->碰撞<-栈增长才会导致堆栈溢出。使用分段栈只是内存不足。(在Go中仍然不会发生溢出,分配器会引发panic) - cthom06
对于C语言,栈的默认大小为1到8 MB,这通常远小于任何计算机内存。实际上,这可能会迫使您避免递归,而递归是最简单的解决方案。 - Mohamed ElNakeep

0

即使是用C语言编写,也只需要满足一些基本影响编译器的限制。

这是一个令人印象深刻的工程成就,但并不是语言设计方面的。


1
我没有任何东西。这个技术基本上是我发明的。只要有一点汇编和想象力,你也可以做到。这并不难。 - Joshua
将esp设置到堆中的某个位置? - Matt Joiner

0

我认为他们在这里所指的是,对数组的访问总是根据数组的实际长度进行检查,从而禁用了C程序意外崩溃或恶意崩溃的最常见方式之一。

例如:

package main

func main() {
    var a [10]int

    for i:= 0; i < 100; i++ {
        a[i] = i
    }
}

当尝试更新不存在的数组第11个元素时,panic会引发运行时错误。C语言会在堆上涂写,并可能崩溃,但是无法控制。每个数组都知道它的长度。在某些情况下,如果编译器可以证明检查不必要,那么就有可能进行优化。(或者一个足够聪明的编译器可以在这个函数中静态地检测到问题。)

其他答案中许多人都在谈论堆栈的内存布局,但这实际上并不相关:您也可以遭受堆溢出攻击。

基本上,Go的指针应始终是类型安全的,包括数组和其他类型,除非您专门使用unsafe包。


5
我觉得你把缓冲区溢出和栈溢出搞混了。不过你说得对。 - cthom06
1
你所描述的是边界检查,与 OP 所问的堆栈溢出无关。Pascal 也会这样做,但在典型实现中容易受到堆栈溢出的影响。 - Fred Foo
我无法确定那里的评论是在谈论堆栈缓冲区溢出还是堆栈溢出。如果我要为了一个懂C语言的观众描述Go的关键特性,我肯定会在提到几乎无限大小的堆栈之前提到已检查的数组。另一方面,这个提及是在一个关于并发的部分,所以也许他们的意思是你可以添加线程而不给它们小的堆栈。 - poolie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接