多图详解Go的sync.Pool源码 (4)

然后将新的节点d2和d互相绑定一下,并将d2设值为头节点,将传入的对象push到d2中;

poolDequeue&pushHead func (d *poolDequeue) pushHead(val interface{}) bool { ptrs := atomic.LoadUint64(&d.headTail) // 解包headTail head, tail := d.unpack(ptrs) // 判断队列是否已满 if (tail+uint32(len(d.vals)))&(1<<dequeueBits-1) == head { return false } // 找到head的槽位 slot := &d.vals[head&uint32(len(d.vals)-1)] // 检查slot是否和popTail有冲突 typ := atomic.LoadPointer(&slot.typ) if typ != nil { return false } if val == nil { val = dequeueNil(nil) } // 将 val 赋值到 slot,并将 head 指针值加 1 *(*interface{})(unsafe.Pointer(slot)) = val atomic.AddUint64(&d.headTail, 1<<dequeueBits) return true }

首先通过位运算判断队列是否已满,也就是将尾部指针加上 len(d.vals) ,因为head指向的是将要被填充的位置,所以head和tail位置是相隔len(d.vals),然后再取低 31 位,看它是否和 head 相等。如果队列满了,直接返回 false;

然后找到找到head的槽位slot,并判断typ是否为空,因为popTail 是先设置 val,再将 typ 设置为 nil,所以如果有冲突,那么直接返回;

最后设值slot,并将head加1返回;

GC

在pool.go文件的 init 函数里,注册了 GC 发生时,如何清理 Pool 的函数:

func init() { runtime_registerPoolCleanup(poolCleanup) } func poolCleanup() { for _, p := range oldPools { p.victim = nil p.victimSize = 0 } for _, p := range allPools { p.victim = p.local p.victimSize = p.localSize p.local = nil p.localSize = 0 } oldPools, allPools = allPools, nil }

poolCleanup 会在 STW 阶段被调用。主要是将 local 和 victim 作交换,那么不至于GC 把所有的 Pool 都清空了,而是需要两个 GC 周期才会被释放。如果 sync.Pool 的获取、释放速度稳定,那么就不会有新的池对象进行分配。

总结

Pool这个概念在后台优化中是一个非常重要的手段,比如说在使用Http的时候会使用Http连接池,使用数据库的时候,也会用到数据库连接池。这些通过对象重用和预先分配可以减少服务器的压力。

当我们在后期的项目开发中,如果发现GC耗时很高,有大量临时对象时不妨可以考虑使用Pool。

例如发现现系统中的 goroutine 数量非常多,由于一个goroutine初始栈是2048字节,所以一个服务器上运行数十万的goroutine 也是非常耗时的;这时候就可以考虑使用Worker Pool 来减少 goroutine 的使用。

Reference

https://medium.com/@genchilu/whats-false-sharing-and-how-to-solve-it-using-golang-as-example-ef978a305e10

https://zhuanlan.zhihu.com/p/99710992

https://morsmachine.dk/go-scheduler

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpdspz.html