33 | 临时对象池sync.Pool
该思维导图由 AI 生成,仅供参考
- 深入了解
- 翻译
- 解释
- 总结
Go语言中的`sync.Pool`是一个临时对象池,用于存储不需要持久使用的临时对象,具有高效的存储和获取机制。它的内部数据结构包括本地池列表,每个本地池包含私有临时对象和共享临时对象列表。通过与P的数量相对应,`sync.Pool`有效地分散存储和性能压力。`Put`方法将临时对象存储到本地池的私有字段,而`Get`方法则优先从私有字段获取临时对象,若为空则访问共享字段。在获取临时对象时,`Get`方法有时会从其他本地池的共享列表中获取。这种存取方式使得`sync.Pool`成为一个特点鲜明的同步工具,适用于实现可伸缩性和作为数据缓存。读者在使用`sync.Pool`时应考虑值的特性,并可通过初始化和方法调用来保证临时对象池中总有充足的临时对象。文章详细介绍了`sync.Pool`的内部机制和使用方式,帮助读者更好地理解和利用这一同步工具。
《Go 语言核心 36 讲》,新⼈⾸单¥59
全部留言(37)
- 最新
- 精选
- 赵赟看了一下 1.14 的源码,那个锁现在是全局的了,即一个临时对象池中本地池列表中的所有本地池都共享一个锁,而不是每个本地池都有自己的锁。
作者回复: 这么说也不准确。看这行源码: shared poolChain poolChain 这个类型的方法会动用原子操作。 再看这行源码: var allPoolsMu Mutex allPoolsMu 会保护单一程序中的所有 sync.Pool,而不是某一个 Pool 的本地池。 然而,sync.Pool 只会在获取 P 的 ID 以及查找对应的 本地池的时候才会动用 allPoolsMu,而在操作本地池的时候没有用。 所以,综上来看,本地池的操作已经通过更好的设计去掉了互斥锁,改为原子操作,同时仅在必要时(也就是定位本地池时)短暂动用互斥锁。 我没去看新 Pool 的性能测试,但是相信一定又有了不小的性能提升。
2020-07-0910 - 小罗希冀请问一下老师, 如果syn.Pool广泛的应用场景是缓存, 那为什么不直接使用map缓存呢?这样岂不是更方便, 更快捷?
作者回复: 你这句话的前后逻辑不通啊,sync.Pool 和 map 是两个东西啊,它们的适用场景完全不一样啊。 sync.Pool 用于缓存“可交换”、“可遗失”的对象。可交换的意思是就是,我用对象 A 也可以,用对象 B 也可以,无所谓。可遗失的意思是,存在里边的对象没了就没了,无所谓,我再创建一个就是了。 map 如果用作缓存的话,其中的元素值是“不可交换”的,通常也是“不可遗失”的(或者说对遗失敏感的)。你思考一下。
2020-10-2627 - 张sir还有一个问题,如果多goruntine同时申请临时对象池内资源,所有goruntine都可以同时获取到吗,还是只能有一个goruntine获取到,其它的goruntine都阻塞,直到这个goruntine释放完后才能使用
作者回复: 我大概明白你的意思。这篇文章你可能还没有仔细看。 你需要先搞清楚(以下内容在文章里都有): 在涉及到本地池的 shared 字段的时候会有锁,但是这种锁是分段锁,也就是说,每个本地池都会有自己的锁。 因此,在对应某个 P 的本地池的锁处于锁定状态的时候,所有正试图访问(不论是 Get 还是 Put)这个本地池的 goroutine 都会被阻塞。 一个临时对象池拥有的本地池的数量与 P 的数量相同。所以,即使有 goroutine 因此被阻塞,往往也只是少数。又因为分段锁的缘故,它们被锁住的时间一般也是很短暂的。 当你知道了这些,你就会明白,临时对象池在并发访问方面是很高效的。 再结合我在专栏里揭示的访问步骤和细节,你应该就可以搞懂你问的问题了。
2019-05-217 - 郭星"在每个本地池中,都包含一个私有的临时对象和一个共享的临时对象列表。前者只能被其对应的 P 所关联的那个 goroutine 中的代码访问到,而后者却没有这个约束" 对于private只能被当前协程才能访问,其他协程不能访问到private,这个应该怎么测试呢? import ( "runtime" "sync" "testing" ) type cache struct { value int } func TestShareAndPrivate(t *testing.T) { p := sync.Pool{} // 在主协程写入10 p.Put(cache{value: 10}) var wg sync.WaitGroup wg.Add(1) go func() { for i := 0; i < 10; i++ { p.Put(cache{value: i}) } wg.Done() }() wg.Wait() wg.Add(1) go func() { for true { v := p.Get() if v == nil { break } t.Log(v) } wg.Done() }() wg.Wait() } 这段代码没有体现出来私有和共享的区别
作者回复: 这个测试比较困难,私有临时对象主要是为了加速对象的存取,但是临时对象池**并不保证**返回给我的对象是按照固定顺序的,你可以认为是随机的。 我们也没必要测试,知道有这样一个加速优化就好了。
2020-09-033 - 越努力丨越幸运老师,当一个goroutine在访问某个临时对象池中一个本地池的shared字段时被锁住,此时另外一个goroutine访问临时对象池时,是会跳过这个本地池,去访问其他的本地池,还是说会被阻塞住?
作者回复: 不会跳过,但是它用的不是锁,而是原子操作,因为存的都是指针。所以速度会非常快。
2020-04-193 - 鲲鹏飞九万里郝老师您好,你在article70.go 的示例中使用sync.Pool 的作用是啥呢,看不出来。你看: func main() { // buf := GetBuffer() // defer buf.Free() buf := &myBuffer{delimiter: delimiter} 在main函数中,我用`buf := &myBuffer{delimiter: delimiter}`这行代码代替上面两行代码后,执行的效果是一样的。 article70.go 的示例,为啥要使用 sync.Pool 呢,麻烦老师进一步讲解下
作者回复: 你每次从这个 sync.Pool 当中,用 Get 方法获取 Buffer,都会得到一个存在于该池中的、已就绪的 Buffer 实例,而 Put 一个 Buffer 实例,则会把它归还给这个 sync.Pool。这是这里演示的 sync.Pool 的基本用法。 这里为了简单和直观,我就没有划分成两个代码包,你可以想象一下,bufPool 变量、Buffer 接口、myBuffer 结构体、GetBuffer 函数同在某一个代码包里面,而 main 函数则存在于另一个代码包里。如此一来,直接写成 &myBuffer{delimiter: delimiter} ,就等于把内部实现暴露给了外界,这样会在后期造成维护成本的增加,况且 myBuffer 还是包级私有的类型。 这里只是一个简单的演示,演示怎样利用 sync.Pool 存储一些可相互替换的同类对象,以供其他程序来高效的获取(Get)和归还(Put)对象。这个 sync.Pool 就是所谓的对象池,其内部实现得非常高效,用法也很简单,不是吗?我们就不用自己实现对象池了,用现成的就好了。 在这个示例中,还包含了一些“便捷函数”(如 GetBuffer 函数)和“便捷方法”(如 myBuffer 的 Free 方法),以方便外界更容易的使用这类 Buffer。 更具体地说,它隐藏了 bufPool 变量(假设该变量不在 main 函数所在的代码包里),并提供了简单调用一下就能获取一个 Buffer 实例的 GetBuffer 函数,以及用完某个 Buffer 就可以直接在它之上调用的 Free 方法(用于将该 Buffer 实例归还给 bufPool)。 总之,这是一种比较好的实现方式,提供给你们参考用的。
2023-02-19归属地:北京2 - 小袁为啥本地池列表长度不是跟M一致,而是跟P一致?
作者回复: 因为P是调度的核心啊,起到了衔接M和G的作用。P实际上也是“并发线”的根数,所以:若少于P数量则未充分利用并发机制,若多于P数量则加重了调度器的负担。
2021-02-132 - 闫飞这里存放的临时对象是否是无状态,无唯一标识符的纯值对象? 对象的类型是否都是一样,还是说必须要用户自己做好具体类型的判定?
作者回复: 你放在一个池子里的实例最好是一个类型的,要不后面用的时候会很麻烦。
2019-07-172 - 苏安老师,不知道还有几讲,最初的课程大纲有相关的拾遗章节,不知道后续的安排还有没?
作者回复: 我会讲完的,放心,预计45讲左右。
2018-10-262 - 传说中的成大大之前学习 go routine的时候 初次了解到这个p以为就是用来调度goroutine的 但是今天又讨论到这个p 这个P还关联到了临时对象池,这个临时对象池也涉及到被运行时系统所清理 所以我产生了以为 这个p时候就是运行时系统呢?
作者回复: 你要想了解Go语言的调度器,可与你参考我写的那本《Go并发编程实战》。(因为一句两句说不清楚)
2020-04-161