Go语言中如何对栈进行处理

本篇内容介绍了“Go语言中如何对栈进行处理”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

网站建设哪家好,找创新互联!专注于网页设计、网站建设、微信开发、微信小程序定制开发、集团企业网站建设等服务项目。为回馈新老客户创新互联还提供了枞阳免费建站欢迎大家使用!

一、线程栈(thread stacks)介绍

在我们研究Go的栈处理方式之前,我们先来看看传统语言,比如C是如何进行栈管理的。

当你启动一个C实现的thread时,C标准库会负责分配一块内存作为这个线程的栈。标准库分配这块内存,告诉内核它的位置并让内核处理这个线程 的执行。不过当这块内存不够用时,问题就来了,我们来看一下下面这个函数:

int a(int m, int n) {     if (m == 0) {         return n + 1;     } else if (m > 0 && n == 0) {         return a(m – 1, 1);     } else {         return a(m – 1, a(m, n – 1));     } }

这个函数大量使用递归,执行a(4, 5)就会降所有栈内存耗尽。要解决这个问题,你可以调整标准库给线程栈分配的内存块的大小。但是全线提高栈大小意味着每个线程都会提高栈的内存使用量,即 便它们不是大量采用递归方式的。这样一来,你将用光所有内存,即便你的程序还尚未使用栈上的内存。

另外一种可选的解决方法则是为每个线程单独确定栈大小。这样一来你就不得不完成这样的任务:根据每个线程的需要,估算它们的栈内存的大小。这将是 创建线程的难度超出我们的期望。想搞清楚一般情况下一个线程栈需要多少内存是不可行的,即便是通常情况也是非常困难的。

二、Go是如何应对这个问题的

Go运行时会试图按需为goroutine提供它们所需要的栈空间,而不是为每个goroutine分配一个固定大小的栈空间。这样可以把程序员 们从决定栈空间大小的烦心事中解脱了出来。不过Go核心团队正在尝试切换到另外一种方案,这里我将尝试阐述旧方案以及它的缺点,新方案以及为何要 做出如此改变。

三、分段栈(Segmented Stacks)

分段栈(segmented stacks)是Go语言最初用来处理栈的方案。当创建一个goroutine时,Go运行时会分配一段8K字节的内存用于栈供goroutine运行使 用,我们让goroutine在这个栈上完成其任务处理。

当我们用光这8K字节的栈空间后,问题随之而来。为了解决这个问题,每个go函数在函数入口处都会有一小段代码(called prologue),这段代码会检查是否用光了已分配的栈空间,如果用光了,这段代码会调用morestack函数。

morestack函数会分配一段新内存用作栈空间,接下来它会将有关栈的各种数据信息写入栈底的一个struct中(译注:下图中Stack info),包括上一段栈的地址。有点我们拥有了一个新的栈段(stack segment),我们将重启goroutine,从导致栈空间用光的那个函数(译注:下图中的Foobar)开始执行。这就是所谓的“栈分裂 (stack split)”。

下面的栈示意图刚好是我们进行栈分裂后的情形:

Go语言中如何对栈进行处理

在新栈的底部,我们插入了一个栈入口函数lessstack。我们不会调用该函数,设置这个函数就是用于我们从那个导致我们用光栈空间的函数(译 注:Foobar)返回时用的。当那个函数(译注:Foobar)返回时,我们回到lessstack(这个栈帧),lessstack会查找 stack底部的那个struct,并调整栈指针(stack pointer),使得我们返回到前一段栈空间。这样做之后,我们就可以将这个新栈段(stack segment)释放掉,并继续执行我们的程序了。

四、分段栈(Segmented stacks)的问题

分段栈给了我们具备按需伸缩能力的栈。程序员们无需担心计算栈的大小了,启动一个新的goroutine代价低廉并且程序员不会知道栈将增长多 大。

这就是直到目前Go语言处理stack增长的方法,但是这个方法有个瑕疵。那就是栈缩小会是一个相对代价高昂的操作。如果你在一个循环遇到栈分裂 (stack split),你会最有感触。一个函数会增加栈空间,做栈分裂,返回并释放栈段(stack segment)。如果你在一个循环中进行这些,你会付出很大的代价(性能方面)。

这就是所谓的“hot split”问题。它也是Go核心开发组更换到一个新的栈管理方案-栈拷贝(stack copying)的主要原因。

五、栈拷贝(stack copying)

栈拷贝初始阶段与分段栈类似。goroutine在栈上运行着,当用光栈空间,它遇到与旧方案中相同的栈溢出检查。但是与旧方案采用的保留一个返 回前一段栈的link不同,新方案创建一个两倍于原stack大小的新stack,并将旧栈拷贝到其中。这意味着当栈实际使用的空间缩小为原先的 大小时,go运行时不用做任何事情。栈缩小是一个无任何代价的操作。此外,当栈再次增长时,运行时也无需做任何事情,我们只需要重用之前分配的空 闲空间即可。

六、栈是怎么拷贝的

拷贝栈听起来简单,但实际上它是一件有难度的事情。因为Go中栈上的变量都有自己的地址,一旦你拥有指向栈上变量的指针,这种情况下你就无法如你 所愿。当你移动栈时,指向原栈的指针都将变为无效指针。

幸运的是,只有在栈上分配的指针才能指向栈上的地址。这点对于内存安全是极其必要的,否则,程序可能会访问到已不再使用了的栈上的地址。

由于我们需要知道那些需要被垃圾收集器回收的指针的位置,因此我们知道栈上哪些部分是指针。当我们移动栈时,我们可以更新栈里地指针使其指向新的 目标地址,并且所有相关的指针都要被照顾到。

由于我们使用垃圾回收的信息来协助完成栈拷贝,因此所有出现在栈上的函数都必须具备这些信息。但事情不总是这样的。因为Go运行时的大部分代码是 用C编写的,大量的运行时调用没有指针信息可用,这样就无法进行拷贝。一旦这种情况发生,我们又不得不退回到分段栈方案,并接受为其付出的高昂代 价。

这就是当前Go运行时开发者大规模重写Go runtime的原因。那些无法用Go重写的代码,比如调度器和垃圾收集器的内核,将在一个特殊的栈上执行,这个特殊栈的size由runtime开发者 单独计算确定。

除了让栈拷贝成为可能之外,这个方法还会使得我们在未来能够实现出并发垃圾回收等特性。

七、关于虚拟内存

另外一种不同的栈处理方式就是在虚拟内存中分配大内存段。由于物理内存只是在真正使用时才会被分配,因此看起来好似你可以分配一个大内存段并让操 作系统处理它。下面是这种方法的一些问题

首先,32位系统只能支持4G字节虚拟内存,并且应用只能用到其中的3G空间。由于同时运行百万goroutines的情况并不少见,因此你很可 能用光虚拟内存,即便我们假设每个goroutine的stack只有8K。

第二,然而我们可以在64位系统中分配大内存,它依赖于过量内存使用。所谓过量使用是指当你分配的内存大小超出物理内存大小时,依赖操作系统保证 在需要时能够分配出物理内存。然而,允许过量使用可能会导致一些风险。由于一些进程分配了超出机器物理内存大小的内存,如果这些进程使用更多内存 时,操作系统将不得不为它们补充分配内存。这会导致操作系统将一些内存段放入磁盘缓存,这常常会增加不可预测的处理延迟。正是考虑到这个原因,一 些新系统关闭了对过量使用的支持。

“Go语言中如何对栈进行处理”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注创新互联网站,小编将为大家输出更多高质量的实用文章!


网页题目:Go语言中如何对栈进行处理
分享路径:http://hbruida.cn/article/iegddc.html