golang sync.Pool试用说明及注意事项

£可爱£侵袭症+ 提交于 2019-12-06 12:40:31

Go tip 是 Go 语言的实验分支,包含了很多尚在讨论,但很有可能会加入 stable 分支的特性。“Go tip 在做什么”(原文地址:What's happening in Go tip)分析总结了 Go 语言尚在开发中的一些重要特性。

本文译自:What's happening in Go tip (2014-01-10)

现在是 2014 年了,刚刚经历了圣诞和新年前夜,Go 团队就已经开始为下一个发布版本而工作了。也因此,“Go tip 在做什么”系列也重开了。

作为这个系列的最新一篇,这篇文章将会有些小调整。最重要的调整是,不会再遵循每周一篇文章的发布周期。一周里可能有几篇文章,也可能一篇都没有。这个调整,一部分由于个人原因,一部分也因为这样可以更灵活的追踪 Go 的改变。这样做的结果是,每篇文章可能会比以前更短,以便能紧跟最新的开发变化。

另一个调整是,将会覆盖一些关于没有变化的代码的形成原因和讨论。这是因为 Go 1.3 将会有重大改变(主要是计划用 Go 重写整个编译器),有些代码需要及早被大家了解。

这篇文章我们将会关注类型sync.Pool。这个类型是 Go 1.3 标准库新添加的第一个重要功能。

做了什么

  • 添加了sync.Pool类型

  • 开发流程的小改变

添加了sync.Pool类型

相关 CL:CL 41860043CL 43990043CL 37720047CL 44080043CL 44150043CL 44060044CL 44050044,CL 44680043CL 46010043

像 JVM 这种项目,花了很多的精力来改进垃圾收集系统,来保证其所要处理回收的众多垃圾。另一方面 Go,大致上采用了在第一时间避免垃圾的设计方法,需要一个不那么时髦的垃圾收集系统,来保证将内存的控制权交还给程序员。

由于这点,标准库里一些包分别实现了重用对象的池,来避免产生过多的垃圾。regexp包为了保证并发时使用同一个正则,而维护了一组状态机,fmt包有众多的打印实例,其他包也有各自的池,或者可以采用这种技术。

不过,这种方法有两个问题。最明显的问题是代码重复:即便重要的代码大都相同,所有的包也需要实现一份自己的池。比较细微的问题是,没有办法回收池持有的空间。这种简单的实现从来不会释放内存,违反了使用垃圾回收的语言的原则,导致过高但不必要的内存使用。

因为这些问题,Brad Fizpatrick曾建议sync包里加入一个公开的Cache类型。这个建议引发了一长串的讨论。Go 语言应该在标准库里提供一个这个样子的类型,还是应当将这个类型作为私下的实现?这个实现应该真的释放内存么?如果释放,什么时候释放?这个类型应当叫做Cache,或者更应该叫做Pool

我先解释一下缓存(cache)和池(pool)的区别,以及为什么这个区别对讨论很重要。Brad Fizpatrick 建议的类型实际上是一种池:一组可以互换的值,取出时并不关心具体的值是什么,因为每个值都是刚被初始化的状态,值是相同的。你甚至分不出来刚刚拿到的值是从池里取出来的,还是新创建的。另一方面,缓存是一些相呼映射的键和值。一个明显的例子是磁盘缓存。磁盘缓存将慢速存储中的文件缓存在系统主内存里,以便提高访问速度。如果缓存里有对应键 A 和 B 的值(磁盘缓存的例子里,就是文件名),而你请求了与 A 对应的值,你显然不想得到 B 所对应的值。实际上,缓存里的值是互不相同的,增加了缓存清除机制的复杂性,就是说到底哪个值应该被清除出缓存。维基百科上关于缓存算法的页面,列举了 13 种不同的清除缓存的算法,从著名的 LRU 缓存到更复杂的比如LIRS 缓存算法

按照这种方式,我们的池真正要关心的问题,只是什么时候回收池占有的空间。而且大家提到了几乎各种可能性:一些在 GC 前回收,一些在 GC 后,基于时钟或者采用弱引用指针。所有的建议都有其弊病。

在经历了漫长的讨论后,Russ Cox 最终提议的 API 和回收策略非常简单:在垃圾收集时回收池空间。这个建议提醒我们,类型Pool的目的是在垃圾收集之间重用内存。它不应该避免垃圾回收,而是让垃圾回收变得更有效。

实现了这个提议,并在几次讨论后,提交到 Go 的代码库。当然,这个 CL 不是最终结果。首先,所有的池都要改写为sync.Pool。这些改写由CL 43990043CL 37720047CL 44080043CL 44150043CL 44060044追踪,但包括CL 44050044CL 44050044关注在尝试将encoding/gob包里使用的本地释放链表替换为sync.Pool。本地是个关键词。一个释放链表会和一个解码器(decoder)的生存时期一样长,直到这个解码器被销毁,才会释放这个链表。Russ Cox回复了这个 CL,明确了sync.Pool的目的,以及它不能用来做什么。直到这时,Rob Pike 提交并回复了CL 44680043,扩展了sync.Pool类型的文档,将其目的描述得更清楚。

Pool设计用意是在全局变量里维护的释放链表,尤其是被多个 goroutine 同时访问的全局变量。使用Pool代替自己写的释放链表,可以让程序运行的时候,在恰当的场景下从池里重用某项值。sync.Pool一种合适的方法是,为临时缓冲区创建一个池,多个客户端使用这个缓冲区来共享全局资源。另一方面,如果释放链表是某个对象的一部分,并由这个对象维护,而这个对象只由一个客户端使用,在这个客户端工作完成后释放链表,那么用Pool实现这个释放链表是不合适的。

从回复(和更早的讨论)来看,加入sync.Pool还是一种试验,如果Pool没有实现它的功能,有可能发布 Go 1.3 之前将其完全移除。这件事情由Issue 6984跟踪。

虽然本文对sync.Pool的探索结束了,但是关于池的讨论还没有结束。还有CL 46010043,为了更适合并发时使用,改进了非常简单的初始化实现。但这个 CL 在目前还没有通过审核。

开发流程的小改变

从 Go 1.3 的周期开始,开发的流程有一些小的变化。这些变化只会影响到直接参与开发流程的人,以及像我一样,紧跟最新变动的人。

启用了一个新的邮件列表,golang-coderreviews,并作为新 CL 的默认抄送对象,替代了原有的golang-dev。这个想法是为了降低 golang-dev 里的噪音,以便让其关注真正的讨论。

同时也启用了一个新的信息板,允许提交者更容易的跟踪还在开放的 Issue 和 CL。任何对 Go 团队的工作方式感兴趣的人,都可以在这个新的信息板上找到有用的说明。


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!