NOTE
2.5 sync.pool
What sync.Pool is, why temporary object caching can reduce allocation and GC pressure, usage, fmt integration, source structures, Get/Put, GC cleanup, and summary.
This is a historical learning note and may contain outdated or incomplete understanding.
1. What It Is
A pool that stores temporary objects.
temp means that an object may be reclaimed, so this pool is actually a cache.
2. Why It Is Needed
Cache temporarily unused objects and use them directly the next time they are needed. This avoids memory allocation and reduces GC pressure.
3. How to Use It
package main
import (
"fmt"
"sync"
)
var pool *sync.Pool
type Person struct {
Name string
}
func (p *Person)Reset() {
p.Name = ""
}
// Initialize the pool. The New function needs to be configured
func initPool() {
pool = &sync.Pool{
New: func() interface{} {
fmt.Println("Creating a new Person")
return new(Person)
},
}
}
func main() {
initPool()
// First Get call: there is no cached object, so call New to create one
p := pool.Get().(*Person)
fmt.Println("First retrieval from pool:", p)
// Initialize
p.Reset()
p.Name = "first"
fmt.Printf("Set p.Name = %s\n", p.Name)
// Put it back into the pool after use
pool.Put(p)
// Second Get call: there is a cached object, so return it directly
fmt.Println("Pool already has one object: &{first}, call Get: ", pool.Get().(*Person))
// Third Get call: there is no cached object, so call New to create one
fmt.Println("Pool has no objects left, call Get: ", pool.Get().(*Person))
}
3.1. fmt
- Printf
func Printf(format string, a ...interface{}) (n int, err error) {
// Output to os.Stdout -- standard output
return Fprintf(os.Stdout, format, a...)
}
- Fprintf
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
p := newPrinter()// create pp
p.doPrintf(format, a)
n, err = w.Write(p.buf)
p.free()// return the pp pointer to the Pool
return
}
func newPrinter() *pp {
// Actually obtains pp from the Pool
p := ppFree.Get().(*pp)
p.panicking = false
p.erroring = false
p.wrapErrs = false
p.fmt.init(&p.buf)
return p
}
var ppFree = sync.Pool{
New: func() interface{} { return new(pp) },
}
func (p *pp) free() {
if cap(p.buf) > 64<<10 {
return
}
// Clear some object fields before returning it to the Pool
p.buf = p.buf[:0]
p.arg = nil
p.value = reflect.Value{}
p.wrappedErr = nil
ppFree.Put(p)
}
4. Source Code Analysis
4.1. Data Structures
- Pool
type Pool struct {
noCopy noCopy
// Local queue for each P; actual type is [P]poolLocal
local unsafe.Pointer // points to the [P]poolLocal array
// Size of [P]poolLocal
localSize uintptr // size of the local array
// When a GC cycle arrives, victim and victimSize respectively "take over" local and localSize. The victim mechanism reduces performance jitter caused by cold start after GC and makes object allocation smoother
victim unsafe.Pointer // local from previous cycle
victimSize uintptr // size of victims array
// Custom object-creation callback. Called when there are no available objects in the pool
New func() interface{}
}
- poolLocal
// Local per-P Pool appendix.
type poolLocalInternal struct {
private interface{} // P's private cache; no lock is needed when using it
shared poolChain // Shared cache. The local P can pushHead/popHead; other Ps can only popTail
}
type poolLocal struct {
poolLocalInternal
// Pad poolLocal to a multiple of two cache lines to prevent false sharing.
// Each cache line has 64 bytes. Our processors generally have 32 KB cache, so there are 32 * 1024 / 64 = 512 cache lines
// False-sharing padding only; prevents multiple poolLocalInternal values from being allocated on one cache line
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
- poolChain
type poolChain struct {
// Only the producer pushes here, so no lock is needed
head *poolChainElt
// Reads and writes require atomic control. Pop from here
tail *poolChainElt
}
type poolChainElt struct {
poolDequeue
// next is written by the producer and read by consumers, so it only changes from nil to non-nil
// prev is written by consumers and read by the producer, so it only changes from non-nil to nil
next, prev *poolChainElt
}
// poolDequeue is implemented as a fixed-size lock-free (atomic implementation) ring queue with a single producer and multiple consumers. The underlying storage is an array, with two pointers marking head and tail. The producer can insert at head and delete from head, while consumers can only delete from tail.
type poolDequeue struct {
// headTail points to the head and tail of the queue. Bit operations store head and tail in the headTail variable.
// headTail contains a 32-bit head and a 32-bit tail pointer. Both values have been reduced modulo len(vals)-1.
// tail is the oldest data in the queue, and head points to the next slot to be filled
// The valid range of slots is [tail, head), owned by consumers.
headTail uint64
// vals is a ring queue storing interface{} values; its size must be a power of 2
// If a slot is empty, vals[i].typ is empty; otherwise it is non-empty.
// A slot is declared invalid when tail no longer points to it and vals[i].typ is nil
// Set to nil by the consumer and read by the producer
vals []eface
}

4.2. Methods
4.2.1. Get
func (p *Pool) Get() interface{} {
// ......
// Call p.pin() to bind the current goroutine to P, disable preemption, and return the poolLocal corresponding to the current P together with pid
l, pid := p.pin()
// Then directly take l.private, assign it to x, and set l.private to nil
x := l.private
l.private = nil
if x == nil {
// If x is empty, try to pop an object from the head of l.shared and assign it to x
x, _ = l.shared.popHead()
if x == nil {
// If x is still empty, call getSlow to try to "steal" an object from the tail of another P's shared deque
x = p.getSlow(pid)
}
}
// Pool-related operations are complete; call runtime_procUnpin() to re-enable preemption
runtime_procUnpin()
// ......
// If no cached object was obtained, directly call the preconfigured New function to create one
if x == nil && p.New != nil {
x = p.New()
}
return x
}

4.2.2. Put
// src/sync/pool.go
// Put adds an object to the Pool
func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
// ……
// First bind g and P, then try to assign x to the private field.
l, _ := p.pin()
if l.private == nil {
l.private = x
x = nil
}
// If that fails, call pushHead to try to put it into the deque maintained by the shared field.
if x != nil {
l.shared.pushHead(x)
}
runtime_procUnpin()
//……
}

4.3. GC
In all pooling techniques, cached objects are cleared at some point.
The init function in pool.go:
func init() {
// Register the function that clears Pool objects when GC occurs
runtime_registerPoolCleanup(poolCleanup)
}
- poolCleanup
func poolCleanup() {
// Clear old pools
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// Move primary cache to victim cache.
// Move all pools into victim
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
oldPools, allPools = allPools, nil
}
5. Summary
sync.Poolis goroutine-safe and very convenient to use. After configuring theNewfunction, callGetto obtain an object andPutto return it- Do not make any assumptions about an object returned by
Get; a better practice is to “clear” the object when returning it - The lifetime of objects in a Pool is affected by GC, so it is not suitable for connection pools, because connection pools need to manage object lifetimes themselves
Discussion
Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub