We have already seen how to implement resource recycling, with a buffered channel with a pool of workers, in the previous chapter. There will be two methods as follows:
- A Get method that tries to receive a message from the channel or return a new instance.
- A Put method that tries to return an instance back to a channel or discard it.
This is a simple implementation of a pool with channels:
type A struct{}
type Pool chan *A
func (p Pool) Get() *A {
select {
case a := <-p:
return a
default:
return new(A)
}
}
func (p Pool) Put(a *A) {
select {
case p <- a:
default:
}
}
We can improve this using the sync.Pool structure, which implements a thread-safe set of objects that can be saved or retrieved. The only thing that needs to be defined is the behavior of the pool when creating a new object:
type Pool struct {
// New optionally specifies a function to generate
// a value when Get would otherwise return nil.
// It may not be changed concurrently with calls to Get.
New func() interface{}
// contains filtered or unexported fields
}
The pool offers two methods: Get and Put. These methods return an object from the pool (or create a new one) and place the object back in the pool. Since the Get method returns an interface{}, the value needs to be cast to the specific type in order to be used correctly. We talked extensively about buffer recycling and in the following example, we will try to implement one using sync.Pool.
We will need to define the pool and functions to obtain and release new buffers. Our buffers will have an initial capacity of 4 KB, and the Put function will ensure that the buffer is reset before putting it back in the pool, as shown in the following code example:
var pool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 0, 4096))
},
}
func Get() *bytes.Buffer {
return pool.Get().(*bytes.Buffer)
}
func Put(b *bytes.Buffer) {
b.Reset()
pool.Put(b)
}
Now we will create a series of goroutines, which will use the WaitGroup to signal when they're done, and will do the following:
- Wait a certain amount of time (1-5 seconds).
- Acquire a buffer.
- Write information on the buffer.
- Copy the content to the standard output.
- Release the buffer.
We will use a sleep time equal to 1 second, plus another second every 4 iterations of the loop, up to 5:
start := time.Now()
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 20; i++ {
go func(v int) {
time.Sleep(time.Second * time.Duration(1+v/4))
b := Get()
defer func() {
Put(b)
wg.Done()
}()
fmt.Fprintf(b, "Goroutine %2d using %p, after %.0fs\n", v, b, time.Since(start).Seconds())
fmt.Printf("%s", b.Bytes())
}(i)
}
wg.Wait()
The information in print also contains the buffer memory address. This will help us to confirm that the buffers are always the same and no new ones are created.