惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
News and Events Feed by Topic
GbyAI
GbyAI
博客园 - Franky
宝玉的分享
宝玉的分享
Blog — PlanetScale
Blog — PlanetScale
Google DeepMind News
Google DeepMind News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
人人都是产品经理
人人都是产品经理
Microsoft Azure Blog
Microsoft Azure Blog
The Register - Security
The Register - Security
腾讯CDC
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
I
InfoQ
The Cloudflare Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
有赞技术团队
有赞技术团队
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
F
Full Disclosure
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Vercel News
Vercel News
博客园 - 【当耐特】
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
S
Schneier on Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Project Zero
Project Zero
量子位
M
MIT News - Artificial intelligence
Stack Overflow Blog
Stack Overflow Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
美团技术团队
Attack and Defense Labs
Attack and Defense Labs
C
Cybersecurity and Infrastructure Security Agency CISA
T
The Blog of Author Tim Ferriss
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
T
Troy Hunt's Blog
Google Online Security Blog
Google Online Security Blog
罗磊的独立博客
P
Proofpoint News Feed
Schneier on Security
Schneier on Security
Spread Privacy
Spread Privacy
S
SegmentFault 最新的问题
L
LINUX DO - 最新话题
Simon Willison's Weblog
Simon Willison's Weblog
爱范儿
爱范儿
博客园 - 聂微东
A
About on SuperTechFans
PCI Perspectives
PCI Perspectives
D
Docker

Posts on Changkun's Blog

Why High-Output Systems Are Often the First to Stop Growing Dark Forest Theory: A Formal Derivation Agents (or Humans) in Goal-Directed and Goalless Environments: On Pipelines, Priors, and the Rhythm Between Exploration and Exploitation At the Boundary of Self-Reference: From Stable Structures in Artificial Intelligence to the Self as a Recursive Model in an Open Dissipative System 2023 Reading List 2022 Reading List 2021 Reading List Are PSS/USS and RSS Actually the Same Thing? Performance Differences from Page Faults vs. Prefetching 2020 Year-End Review Migration with Zero Downtime 2020 Reading List The All in Go Stack Pointers Might Not Be Ideal for Parameters Eliminating A Source of Measurement Errors in Benchmarks Setup Wordpress in 10 Minutes 我为什么不再写博客了? 2019 年终总结 2018-2019 读书清单 Ten years of blogging Rethinking the Reflections on Communications and Trusts 2018 年终总结 Go source code study is open source Go source study: unsafe Pattern Go source study: sync.Pool A Million WebSocket and Go Designing Asynchronous RESTful APIs 分布式杂谈01:CAP 理论的误解 Issues of Human-Bot Interaction 压缩法与深度网络的泛化性 Go in 1 Hour UMSLT04: The Past and Present of SGD UMSLT03: A Gentle Start of Learning Theory UMSLT02: A Breif History of Neural Networks UMSLT01: A Breif History of Regularization 不笑不足以为道 论文笔记:Generalization in Deep Learning 2017 年终总结 2017 读书清单 深度学习的泛化理论简介 删除 GitHub 上已经提交的敏感信息 硕士生涯的第一年就这样告一段落了 人肉计算(10): 系统参与激励 人肉计算(9): 陷阱的解法 别聊,一聊你就暴露 人肉计算(8): 人肉计算与数据科学中的陷阱 人肉计算(7): 社会行为分析 Hexo + GitHub + Travis CI + VPS 自动部署 人肉计算(6): 预测市场 人肉计算(5): 信用风险评级模型 读书与回报 瞎扯: 对现代企业理论与当下IT企业的商业模式和信息产业链的规律性的思考 人肉计算(4): 输入数据聚合与PageRank 又一次打整了一下博客 人肉计算(3): 输入数据聚合与链路预测 人肉计算(2): 意图博弈 GWAPs 人肉计算(1): 众包与群众智慧 对后辈同学在计算机专业上的答疑与解惑 在德国的医疗及住院体验 这可能不是一个技术博客了 实验楼楼赛第3期-Python-题解 迅速更换了 DISQUS Electron 深度实践总结 良好的编码体验的三个方面 2016 年终总结 2016 读书清单 最近在着手写的文章 微信小程序文档极致总结 谈谈过去三个月在实验楼的实习经历 Built a Desktop Client for My Blog Guacamole 源码分析与 VNC 中 RFB 协议的坑 《高速上手 C++11/14》正式发布 Docker 极速入门教程02 - 镜像与容器管理 Docker 极速入门教程01 - 基本概念和操作 阶段性沉默 ELK+Redis 最佳实践 终于全面启用了 HTTPS 苹果开源了LZFSE无损压缩 Hash 碰撞的一种思路 记一次完整的 Kaldi-TIMIT 示例运行 Kaldi 上的 TIMIT 例子 Kaldi 安装与部署 从科研写作谈起 Swift API 设计指南 有趣的人类 所以其实论文并没有什么鬼用 Githug 通关记录及指南 小结一下这学期的收获 2015 读书清单 2015 年终总结 负能量爆表 转眼就快两个月了 博客迁移记录 大三总结 这个世界,终究不会是我们的。 Linux 内核分析 之六:Linux 内核创建进程的过程 小说「泽缘」 Linux 内核分析 之五:system_call中断处理过程的简要分析 大创项目的标题真是每年都在考验同学们的想象力啊 Linux 内核分析 之四:使用库函数API和嵌入汇编两种方式使用同一个系统调用
Go runtime programming
Changkun Ou · 2018-09-23 · via Posts on Changkun's Blog

Published at发布于:   |   PV/UV: /   |   Reading阅读: 3 min

Go 运行时编程

本文可能会过时。 它旨在阐明不同于常规 Go 编程的 Go 运行时编程,并侧重于一般概念而非特定接口的详细信息。

调度器结构

调度器管理了三种不同类型的资源分布在运行时中:G, M, P。 即便你进行不涉及调度器的相关工作,理解它们仍然很重要。

Gs, Ms, Ps

“G” 是 goroutine 的缩写。由 g 类型表示。当 goroutine 退出时,g 被归还到有效的 g 池, 能够被后续 goroutine 使用。

“M” 代表一个能够执行用户 Go 代码、运行时代码、系统调用或处于空闲状态的 OS thread。 由类型 m 表示。因为任意多个线程可以同时阻塞在系统调用上,因此同一时刻可以包含任意多个 M。

“P” 则表示执行 Go 代码所需的资源,例如调度器和内存分配器状态。由 p 类型表示,且最多只有 GOMAXPROCS 个。 P 可以理解一个 OS 调度器中的 CPU,p 类型的内容类似于每个 CPU 的状态。是一个可以为了效率 而存放不同状态的地方,同时还不需要每个线程或者每个 goroutine 对应一个 P。

调度器的任务是匹配一个 G(要执行的代码)一个 M(在哪儿执行)和一个 P(执行代码的资源和权利)。 当 M 停止执行用户 Go 代码时,例如进入系统调用,它会将其对应的 P 返回给空闲 P 池。 而为了恢复执行用户的 Go 代码,例如从一个系统调用返回时,必须从空闲池中获取一个有效的 P。

所有的 g, mp 对象均在堆上分配,且从不释放。因此他们的内存保持 type stable。 因此,运行时可以在调度器内避免 write barrier。

用户栈与系统栈

每个非 dead 状态的 G 均被关联到用户栈上,即用户 Go 代码执行的地方。用户栈初始大小很小(例如 2K),然后动态的增加或减少。

每个 M 均对应一个关联的系统栈(也成为 M 的 g0 栈,因为其作为一个 stub G 来实现)。在 Unix 平台上,则称为信号栈(也称之为 M 的 gsignal 栈)。系统和信号栈无法扩展,但已经大到足够运行运行时和 cgo 代码(一个纯 Go 二进制文件有 8K;而 cgo 二进制文件则有系统分配)。

运行时代码经常会临时通过 systemstack, mcallasmcgocall 切换到系统栈以执行那些无法扩展用户栈、或切换用户 goroutine 的不可抢占式任务。运行在系统栈上的代码是隐式不可抢占的、且不会被垃圾回收检测。当运行在系统栈上时,当前用户栈没有被用于执行代码。

getg()getg().m.curg

为获取当前用户 g,可以使用 getg().m.curg

getg() 单独返回当前 g。但当执行在系统或信号栈时,会返回当前 M 的 g0gsignal。这通常可能不是你想要的。

为判断你是运行在用户栈还是系统栈,可以使用:getg() == getg().m.curg

错误处理与报告

通常使用 panic 的来自用户代码的错误能够被合理的恢复。然而某些情况下 panic 会导致一个立即致命错误,例如在系统栈上的调用或在 mallocgc 执行阶段的调用。

大部分运行时错误无法恢复。对于这些错误,请使用 throw,它能够将被终止整个过程的中间状态全部回溯出来。一般情况下,throw 需要传递一个 string 常量在危险情况下避免内存分配。按照惯例,在 throw 前会使用 printprintln 输出额外信息,并冠以 “runtime:” 的前缀。

若需调试运行时错误,可以设置 GOTRACEBACK=systemGOTRACEBACK=crash

同步

运行时有多种同步机制。它们根据与 goroutine 调度器或者 OS 调度器的交互不同而有着不同的语义。

最简单的是 mutex,使用 lockunlock 进行操作。用于在短时间内共享结构体。在 mutex 上阻塞会直接阻塞 M,且不会与 Go 调度器进行交互。这也就意味着它能在运行时最底层是安全的,且同时阻止了任何与之关联的 G 和 P 被重新调度。rwmutex 也类似。

为进行一次性通知,可以使用 note。它提供了 notesleepnotewakeup。与传统的 UNIX sleep/wakeup 不同,note 是无竞争的。因此notesleep 会在 notewakeup 发生时立即返回。一个 note 能够在使用 noteclear 后被重置,并且必须不能与 sleep 或 wakeup 产生竞争。类似于 mutex,阻塞在 note 上会阻塞 M。然而有很多不同的方式可以在 note 上进行休眠:notesleep 会阻止关联的 G 和 P 被重新调度,而 notetsleepg 的行为类似于阻止系统调用、运行 P 被复用从而运行另一个 G。这种方式仍然比直接 阻塞一个 G 效率低,因为它还消耗了一个 M。

为直接与 goroutine 调度器进行交互,可以使用 goparkgoreadygopark 停摆了一个当前的 goroutine,并将其放入 waiting 状态,并将其从调度器运行队列中移除,再进一步将其他 goroutine 在当前 M/P 上进行调度。而 goready 会将一个停摆的 goroutine 标回 runable 状态,并加入运行队列中。

总结:

阻塞
接口GMP
(rw)mutex
note是/否
park

原子操作

运行时拥有自己的原子操作包,位于 runtime/internal/atomic。其对应于 sync/atomic,但处于历史原因函数具有不同的名字以及一些额外的运行时所需的函数。

一般来说,我们都仔细考虑了在运行始终使用这些原子操作,并尽可能避免了不必要的原子操作。如果某个时候访问一个某些时候会被其他同步机制保护的变量,那么访问已经受保护的内容通常不需要成为原子操作,原因如下:

  1. 在适当的时候使用非原子或原子操作访问会使代码更明确。对于变量的原子访问意味着其他地方可能同时访问该变量。
  2. 非原子访问能够自动进行竞争检查。运行时目前并没有竞争检查器,但未来可能会有。运行竞争检查器来检查你非原子访问的假设时,原子访问会破坏竞争检查器。
  3. 非原子访问能够提升性能。

当然,对一个共享变量进行任何非原子访问需要解释为什么该访问是受到保护的。

混合原子访问和费原子访问的常见模式为:

  • 读通过写锁进行的变量,在锁区域内,读不需要原子操作,而写是需要的。在锁定区外,读是需要原子操作的。

  • 读取仅发生在 STW 期间(Stop-The-World)。在 STW 期间不会发生写入 STW,即不需要原子操作。

换句话说,Go 内存模型的建议是:『不要太聪明』。运行时的性能很重要,但它的健壮性更重要。

非托管内存

通常,运行时尝试使用常规堆分配。然而,在某些情况下,运行时必须非托管内存中分配垃圾回收堆之外的对象。如果对象是内存管理器的一部分,或者必须在调用者可能没有 P 的情况下分他们,则这些分配和回收是有必要的。

分配非托管内存有三种机制:

  • sysAlloc 直接从 OS 获取内存。这会是系统页大小的整数倍,但也可以使用 sysFree 进行释放。

  • persistentalloc 将多个较小的分配组合到一个 sysAlloc 中避免碎片。但没有办法释放 persistentalloc 对象(所以叫这个名字)。

  • fixalloc 是一个 SLAB 样式的分配器,用于分配固定大小的对象。fixalloced 对象可以被释放,但是这个内存只能由同一个 fixalloc 池重用,所以它只能被重用于同一类型的对象。

一般来说,使用其中任何一个分配的类型应标记为 //go:notinheap (见下文)。

在非托管内存中分配对象不得包含堆指针,除非遵循下列原则:

  1. 任何来自非托管内存的堆指针必须在 runtime.markroot 中添加为显式垃圾回收的 root。
  2. If the memory is reused, the heap pointers must be zero-initialized before they become visible as GC roots. Otherwise, the GC may observe stale heap pointers. See “Zero-initialization versus zeroing”. 如果内存被重用,那么堆指针必须进行在他们作为 GC root 可见前进行零初始化。否则,GC 可能会回收已经过时的堆指针。请参考「零初始化与归零」

零初始化 v.s. 归零

运行时有两种类型的归零方式,具体取决于内存是否已经初始化为类型安全状态。

如果内存不是类型安全的状态,那么它可能包含「垃圾」,因为它刚刚被分配且被初始化以供第一次使用,因此它必须使用 memclrNoHeapPointers 或非指针写进行零初始化。这不会执行 write barrier。

如果内存已经处于类型安全状态,并且只设置为零值,则必须使用常规写,通过 typedmemclrmemclrHasPointers 完成。这会执行 write barrier。

运行时独占的编译标志

除了 “go doc compile” 文档中说明的 “//go:” 标志外,编译器还未运行时支持了额外的标志。

go:systemstack

go:systemstack 表示函数必须在系统堆栈上运行。由特殊的函数序言(function prologue,指汇编程序函数开头的几行代码,通常是寄存器准备)进行动态检查。

go:nowritebarrier

如果函数包含 write barrier,则 go:nowritebarrier 触发一个编译器错误(它不会抑制 write barrier 的产生,只是一个断言)。

你通常希望 go:nowritebarrierrecgo:nowritebarrier 主要适用于没有 write barrier 会更好的情况,但没有要求正确性。

go:nowritebarrierrec 和 go:yeswritebarrierrec

.如果声明的函数或任何它递归调用的函数甚至于 go:yeswritebarrierrec 包含 write barrier,则 go:nowritebarrierrec 触发编译器错误。

逻辑上,编译器为每个函数调用补充 go:nowritebarrierrec 且当遭遇包含 write barrier 函数的时候产生一个错误。这种补充在 go:yeswritebarrierrec 函数上停止。

go:nowritebarrierrec 用于防止 write barrier 实现中的无限循环。

两个标志都在调度器中使用。write barrier 需要一个活跃的 P (getg().m.p != nil)且调度器代码通常在没有活跃 P 的情况下运行。在这种情况下,go:nowritebarrierrec 用于释放 P 的函数上,或者可以在没有 P 的情况下运行。而且go:nowritebarrierrec 还被用于当代码重新要求一个活跃的 P 时。由于这些都是函数级标注,因此释放或获取 P 的代码可能需要分为两个函数。

这两个指令都在调度程序中使用。 write barrier 需要一个活跃的P( getg().mp != nil)并且调度程序代码通常在没有活动 P 的情 况下运行。在这种情况下,go:nowritebarrierrec用于释放P的函数或者可以在没有P的情况下运行并且去 :当代码重新获取活动P时使用yeswritebarrierrec。由于这些是功能级注释,因此释放或获取P的代码可能需要分为两个函数。

go:notinheap

go:notinheap 适用于类型声明。它表明一种不能从 GC 堆中分配的类型。具体来说,指向此类型必须让 runtime.inheap 检查失败。类型可能是用于全局变量,堆栈变量或用于对象非托管内存(例如使用 sysAlloc 分配、persistentallocfixalloc 或手动管理的范围)。特别的:

  1. new(T), make([]T), append([]T, ...) 以及 T 的隐式堆分配是不允许的(尽管运行时中无论如何都是不允许隐式分配的)。

  2. 指向常规类型( unsafe.Pointer 除外)的指针不能转换为指向 go:notinheap 类型,即使他们有相同的基础类型。

  3. 任何包含 go:notinheap 类型的类型本身也是 go:notinheap 的。结构和数组中如果元素是 go:notinheap 的则自生也是。go:notinheap 类型的 map 和 channel 是不允许的。为使所有事情都变得显式,任何隐式 go:notinheap 类型的声明必须显式的声明 go:notinheap

  4. 指向 go:notinheap 类型的指针上的 write barrier 可以省略。

最后一点是 go:notinheap 真正的好处。运行时会使用它作为低级别内部结构使用来在内存分配器和调度器中避免 非法或简单低效的 memory barrier。这种机制相当安全且没有牺牲运行时代码的可读性。

原文地址:Go runtime programming

译者:Changkun Ou