33 | 临时对象池sync.Pool
33 | 临时对象池sync.Pool
讲述:黄洲君
时长15:11大小6.95M
问题解析
知识扩展
问题 1:临时对象池存储值所用的数据结构是怎样的?
问题 2:临时对象池是怎样利用内部数据结构来存取值的?
总结
思考题
赞 13
提建议
精选留言(36)
- 虢國技醬2020-01-09go1.13对本地池的shared共享列表做了存储结构变更,改为双向链表(在shared的头部存,尾部取),取消锁以提高性能共 2 条评论27
- 数字记忆2019-11-20这个代码很形象: package main import ( "fmt" "sync" "time" ) // 一个[]byte的对象池,每个对象为一个[]byte var bytePool = sync.Pool{ New: func() interface{} { b := make([]byte, 1024) return &b }, } func main() { a := time.Now().Unix() fmt.Println(a) // 不使用对象池 for i := 0; i < 1000000000; i++{ obj := make([]byte,1024) _ = obj } b := time.Now().Unix() fmt.Println(b) // 使用对象池 for i := 0; i < 1000000000; i++{ obj := bytePool.Get().(*[]byte) _ = obj bytePool.Put(obj) } c := time.Now().Unix() fmt.Println(c) fmt.Println("without pool ", b - a, "s") fmt.Println("with pool ", c - b, "s") } // run时禁用掉编译器优化,才会体现出有pool的优势 // go run -gcflags="-l -N" testSyncPool1.go展开共 1 条评论16
- 到不了的塔2018-11-17临时对象池初始化时指定new字段对应的函数返回一个新建临时对象; 临时对象使用完毕时调用临时对象池的put方法,把该临时对象put回临时对象池中。 这样就能保证一个临时对象池中总有比较充足的临时对象。11
- 赵赟2020-07-09看了一下 1.14 的源码,那个锁现在是全局的了,即一个临时对象池中本地池列表中的所有本地池都共享一个锁,而不是每个本地池都有自己的锁。
作者回复: 这么说也不准确。看这行源码: shared poolChain poolChain 这个类型的方法会动用原子操作。 再看这行源码: var allPoolsMu Mutex allPoolsMu 会保护单一程序中的所有 sync.Pool,而不是某一个 Pool 的本地池。 然而,sync.Pool 只会在获取 P 的 ID 以及查找对应的 本地池的时候才会动用 allPoolsMu,而在操作本地池的时候没有用。 所以,综上来看,本地池的操作已经通过更好的设计去掉了互斥锁,改为原子操作,同时仅在必要时(也就是定位本地池时)短暂动用互斥锁。 我没去看新 Pool 的性能测试,但是相信一定又有了不小的性能提升。
9 - 张sir2019-05-21还有一个问题,如果多goruntine同时申请临时对象池内资源,所有goruntine都可以同时获取到吗,还是只能有一个goruntine获取到,其它的goruntine都阻塞,直到这个goruntine释放完后才能使用
作者回复: 我大概明白你的意思。这篇文章你可能还没有仔细看。 你需要先搞清楚(以下内容在文章里都有): 在涉及到本地池的 shared 字段的时候会有锁,但是这种锁是分段锁,也就是说,每个本地池都会有自己的锁。 因此,在对应某个 P 的本地池的锁处于锁定状态的时候,所有正试图访问(不论是 Get 还是 Put)这个本地池的 goroutine 都会被阻塞。 一个临时对象池拥有的本地池的数量与 P 的数量相同。所以,即使有 goroutine 因此被阻塞,往往也只是少数。又因为分段锁的缘故,它们被锁住的时间一般也是很短暂的。 当你知道了这些,你就会明白,临时对象池在并发访问方面是很高效的。 再结合我在专栏里揭示的访问步骤和细节,你应该就可以搞懂你问的问题了。
7 - 小罗希冀2020-10-26请问一下老师, 如果syn.Pool广泛的应用场景是缓存, 那为什么不直接使用map缓存呢?这样岂不是更方便, 更快捷?
作者回复: 你这句话的前后逻辑不通啊,sync.Pool 和 map 是两个东西啊,它们的适用场景完全不一样啊。 sync.Pool 用于缓存“可交换”、“可遗失”的对象。可交换的意思是就是,我用对象 A 也可以,用对象 B 也可以,无所谓。可遗失的意思是,存在里边的对象没了就没了,无所谓,我再创建一个就是了。 map 如果用作缓存的话,其中的元素值是“不可交换”的,通常也是“不可遗失”的(或者说对遗失敏感的)。你思考一下。
共 2 条评论7 - 虢國技醬2019-11-29二刷共 2 条评论5
- 小袁2021-02-13为啥本地池列表长度不是跟M一致,而是跟P一致?
作者回复: 因为P是调度的核心啊,起到了衔接M和G的作用。P实际上也是“并发线”的根数,所以:若少于P数量则未充分利用并发机制,若多于P数量则加重了调度器的负担。
2 - 郭星2020-09-03"在每个本地池中,都包含一个私有的临时对象和一个共享的临时对象列表。前者只能被其对应的 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() } 这段代码没有体现出来私有和共享的区别展开
作者回复: 这个测试比较困难,私有临时对象主要是为了加速对象的存取,但是临时对象池**并不保证**返回给我的对象是按照固定顺序的,你可以认为是随机的。 我们也没必要测试,知道有这样一个加速优化就好了。
3 - 越努力丨越幸运2020-04-19老师,当一个goroutine在访问某个临时对象池中一个本地池的shared字段时被锁住,此时另外一个goroutine访问临时对象池时,是会跳过这个本地池,去访问其他的本地池,还是说会被阻塞住?
作者回复: 不会跳过,但是它用的不是锁,而是原子操作,因为存的都是指针。所以速度会非常快。
2 - Lywane2020-03-31直到看到最近两三章,我才体会到,老师就是在讲源码啊!对着源码学习课程,对着课程学习源码。事半功倍!!2
- 闫飞2019-07-17这里存放的临时对象是否是无状态,无唯一标识符的纯值对象? 对象的类型是否都是一样,还是说必须要用户自己做好具体类型的判定?
作者回复: 你放在一个池子里的实例最好是一个类型的,要不后面用的时候会很麻烦。
2 - 苏安2018-10-26老师,不知道还有几讲,最初的课程大纲有相关的拾遗章节,不知道后续的安排还有没?
作者回复: 我会讲完的,放心,预计45讲左右。
2 - lesserror2021-08-21意外之喜,隔壁专栏鸟窝的《Go 并发编程实战课》对 sync.Pool 有了新的补充,这一讲有困惑的同学可以过去康康。1
- 传说中的成大大2020-04-16之前学习 go routine的时候 初次了解到这个p以为就是用来调度goroutine的 但是今天又讨论到这个p 这个P还关联到了临时对象池,这个临时对象池也涉及到被运行时系统所清理 所以我产生了以为 这个p时候就是运行时系统呢?
作者回复: 你要想了解Go语言的调度器,可与你参考我写的那本《Go并发编程实战》。(因为一句两句说不清楚)
1 - 疯琴2020-01-02请问老师,demo70 的 37 行 return 后面没跟东西,是相当于 return nil 么?
作者回复: 不是,是返回结果变量err的值。
1 - 来碗绿豆汤2018-10-28是不是临时对象池里面最多有2p个临时对象1
- Haij!2022-08-25 来自四川fmt包为了识别、格式化和暂存需要打印的内容,定义了一个名为pp的结构体。调用不同的打印方法时,都需要一个pp的结构体介入逻辑进行处理;如果未使用sync.Pool,则每次均会通过new函数初始化pp类型的变量,这时会频繁申请内存。所以为避免每次需要时都调用pp的new方法申请内存,故基于sync.Pool创建一个临时对象池。当打印操作很活跃时,可以直接从池中获取pp结构体并使用;使用后抹取过程中的信息再存入。一方面可以利用“缓存”特性进行性能提升,避免频繁内存申请分配;另一方面可以借由sync.Pool初始时在运行时系统注册的cleanPool方法,及时清理空间,释放内存。展开
- 林嘉裕2021-12-21数组可以通过put(arr[:0])清空,如果是map呢?只能通过遍历?
作者回复: 你要是真是想清空,重新 Put 一个空的 map 就好了啊,切片的话也可以这样操作。只要这个 map 或者 slice 再没有别的代码引用它了,GC 就会进行回收了。
- jxs12112021-10-30由于fmt包中的代码在真正使用这些临时对象之前,总是会先对其进行重置, func newPrinter() *pp { p := ppFree.Get().(*pp) p.panicking = false p.erroring = false p.wrapErrs = false p.fmt.init(&p.buf) return p } 思考:这段重置的代码为什么不能放到使用完成后,一并p.free func (p *pp) free() { // Proper usage of a sync.Pool requires each entry to have approximately // the same memory cost. To obtain this property when the stored type // contains a variably-sized buffer, we add a hard limit on the maximum buffer // to place back in the pool. // // See https://golang.org/issue/23199 if cap(p.buf) > 64<<10 { return } p.buf = p.buf[:0] p.arg = nil p.value = reflect.Value{} p.wrappedErr = nil ppFree.Put(p) }展开
作者回复: 这里的 free() 是为了重用当前的这个 p ,每个 p 放回 ppFree 的时机都不一样,怎么统一 free 呢?即使可以,也没什么切实的好处啊。