文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Go语言中怎么对栈进行处理

2024-04-02 19:55

关注

本篇文章为大家展示了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语言中怎么对栈进行处理,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注编程网行业资讯频道。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     807人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     351人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     314人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     433人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     221人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-前端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯