Description
The operation of sync.Pool
assumes that the memory cost of each element is approximately the same in order to be efficient. This property can be seen by the fact that Pool.Get
returns you a random element, and not the one that has "the greatest capacity" or what not. In other words, from the perspective of the Pool
, all elements are more or less the same.
However, the Pool
example stores bytes.Buffer
objects, which have an underlying []byte
of varying capacity depending on how much of the buffer is actually used.
Dynamically growing an unbounded buffers can cause a large amount of memory to be pinned and never be freed in a live-lock situation. Consider the following:
pool := sync.Pool{New: func() interface{} { return new(bytes.Buffer) }}
processRequest := func(size int) {
b := pool.Get().(*bytes.Buffer)
time.Sleep(500 * time.Millisecond) // Simulate processing time
b.Grow(size)
pool.Put(b)
time.Sleep(1 * time.Millisecond) // Simulate idle time
}
// Simulate a set of initial large writes.
for i := 0; i < 10; i++ {
go func() {
processRequest(1 << 28) // 256MiB
}()
}
time.Sleep(time.Second) // Let the initial set finish
// Simulate an un-ending series of small writes.
for i := 0; i < 10; i++ {
go func() {
for {
processRequest(1 << 10) // 1KiB
}
}()
}
// Continually run a GC and track the allocated bytes.
var stats runtime.MemStats
for i := 0; ; i++ {
runtime.ReadMemStats(&stats)
fmt.Printf("Cycle %d: %dB\n", i, stats.Alloc)
time.Sleep(time.Second)
runtime.GC()
}
Depending on timing, the above snippet takes around 35 GC cycles for the initial set of large requests (2.5GiB) to finally be freed, even though each of the subsequent writes only use around 1KiB. This can happen in a real server handling lots of small requests, where large buffers allocated by some prior request end up being pinned for a long time since they are not in Pool
long enough to be collected.
The example claims to be based on fmt
usage, but I'm not convinced that fmt
's usage is correct. It is susceptible to the live-lock problem described above. I suspect this hasn't been an issue in most real programs since fmt.PrintX
is typically not used to write very large strings. However, other applications of sync.Pool
may certainly have this issue.
I suggest we fix the example to store elements of fixed size and document this.