golang sync.Pool的数据模型在1.13中发生了比较大的改变,一方面是数据模型的重构,一方面是GC对sync.Pool池子影响的优化。
1.数据模型:
初始情况
-
默认大小为8,能放8个item。
-
因为是有限大小的FIFO,所以采取了最佳模型ring buffer来实现这个队列。(因为定长队列是用定长数组实现的,如果在头部push的话,需要后续所有内容往后迁移,复杂度为O(n),而使用环状的话则复杂度为O(1)。)
如图:
(1)O(n)——普通队列
(2)O(1)——环状队列
https://en.wikipedia.org/wiki/Circular_buffer
-
head端只能一个producer,即当前线程,进行push和pop,tail端可以被多个comsumer,即其他所有线程,进行pop操作。(多个comsumer并发读取采用的是atomic加锁的方式限制,如每次pop时对index进行atomic.CompareAndSwapUint64操作,比较轻量)。
-
tail的每次读取都会删除该项,将这一项变成nil。(也就是说如果读取的速度足够快,则始终就在这大小为8的环里运行,节省空间)
-
head的每次写入val,即使是nil,也会转变为*struct{}作为标记。
如果写满了
- 如果写的速度比较快,写满了之后,则根据双向链表的方式,引出一个2倍大小的ring buffer(每次都会翻倍,直到达到最大为1<<32 / 4则不再增加)。
- 当前的ring如果pop head用完了,则会根据prev查看上一个ring
- 如果tail pop完了之后,则会删除这个ring,随后指向新的ring(因为后续不再使用)
sync.Pool1.13模型设计亮点:
- 使用了ring buffer(定长FIFO) + 双向链表的方式,头部只能当前线程读取与写入,尾部可以并发读取,相较于1.12版一个数组,加上一个大锁限制,轻量的多,效率高了许多。
- head与tail的index使用一个uint64存储,前32位是head,后32位是tail。通过掩码以及位运算获取他俩,然后在atomic.CompareAndSwapUint64的操作中效率更高,一个操作就能比较head和tail的index是否发生了改变。
2. GC优化
https://medium.com/a-journey-with-go/go-understand-the-design-of-sync-pool-2dde3024e277
之前每次GC时都会清空pool,而在1.13版本中引入了victim cache,会将pool内数据拷贝一份,避免GC将其清空,即使没有引用的内容也可以保留最多两轮GC。
相关性能测试:
https://www.jianshu.com/p/cf3ac244e222