[concurrent programming] basic usage and implementation of Pool

Posted by XaeroDegreaz on Thu, 20 Jan 2022 07:58:45 +0100

Blogger introduction:

- I am the official account of WeChat. The Milky way ]I look forward to your attention. Come on in the future~

preface

Go is a programming language for automatic garbage collection. It uses three-color concurrent marking algorithm to mark objects and recycle them. Therefore, we generally use it whenever we want, without considering how to improve performance. However, if you want to use go to develop a high-performance application, you must consider the performance impact of garbage collection.

But what impact does it have? Go's automatic garbage collection mechanism has an STW (stop the world) time, and another time-consuming problem is that a large number of objects created on the heap will also affect the garbage collection marking time.

Generally, when performing performance optimization, you choose to manually recycle unused objects to avoid being garbage collected, so that you don't have to recreate them on the heap the next time you use them. In addition, for example, database connections and long TCP connections can be saved to avoid re creation every time they are used, which can not only greatly reduce the time-consuming of the business, but also improve the overall performance of the application.

Therefore, in order to solve the above problems, the GO standard library provides a general pool data structure, that is, sync Pool, which allows you to create pooled objects. However, this type has a disadvantage that its pooled objects may be garbage collected, which is not suitable for scenarios such as long database connections.

sync.Pool

sync. The pool data type is used to hold a set of temporary objects that can be accessed independently. Temporary means that pooled objects will be removed without warning at some time in the future. Moreover, if no other object references the materialization of the removed object, the removed object will be garbage collected.

Knowledge points:

  • sync.Pool itself is thread safe. Multiple goroutine s can call its methods to access objects concurrently;
  • sync.Pool cannot be copied and used after use.

sync. How to use pool

This data type provides three external methods: New, Get, and Put.

  • New
    The type of this field is the function func() interface {}. When the Get method of Pool is called to Get elements from the Pool and there are no more free elements to return, this New method will be called to create New elements. If the New field is not set and there are no more free elements to return, the Get method will return nil, indicating that there are no available elements at present.
  • Get
    If the Get method is called, an element will be taken from the Pool, and the take away finger will be removed from the Pool and returned to the caller. However, except that the return value is a normally instantiated element, the return value of the Get method may also be a nil (the Pool. New field is not set, and there are no idle elements to return), so you need to judge when using it.
  • Put
    Put is used to store an element in the Pool. The Pool will save the element in the Pool and can be reused. However, if put a nil value, Pool ignores this value.
  • buffer pool
    The buffer pool is sync Pool is the most commonly used scenario.
    Because byte slice is a kind of object that is often created and destroyed, the created byte slice can be cached by using buffer pool. For example, the famous static website generation tool Hugo includes such an implementation bufpool.
var buffers = sync.Pool{
    New: func() interface{}{
        return new(bytes.Buffer)
    },
}

func GetBuffer() *bytes.Buffer {
    return buffers.Get().(*bytes.Buffer)
}

func PutBuffer(buf *bytes.Buffer) {
    buf.Reset()
    buffers.Put(buf)
}

This code is very common, but there is a problem. Please do not use it in the project. There may be a memory leak in this code.

Implementation principle

  1. Each GC recycles the created object.
    If the number of cache elements is too large, STW will take longer; After all cache elements are recycled, the Get hit rate will decrease, and the Get method has to create many new objects.

  2. The underlying implementation uses Mutex. When competing for records for concurrent requests for this lock, it will lead to performance degradation.
    sync.Pool has done a lot of optimization.
    The optimization point to improve the performance of concurrent programs is to try not to use locks. If locks have to be used, the granularity of lock go will be minimized. Go's optimization of the Pool is to avoid the use of locks. At the same time, the implementation of changing the locked queue to lock free queue gives the elements to be removed another chance to "revive".

sync. The data structure of pool is shown in the following figure:

The two most important fields of Pool are local and victim, because their two main functions are to store idle elements.

During each garbage collection, the Pool will talk about the object collection in victim, and then give the local data to victim. In this way, the local will be emptied, and victim will be like a garbage sorting station. The things inside may be discarded as garbage, but the useful things inside may also be picked up and reused.

If the element in victim is taken away by Get, the element will be lucky because it is "resurrected" again. However, if the concurrency of Get is not very large at this time and the element is not taken away by Get, it will be removed, because if no one else references it, it will be garbage collected.

This code is sync. When garbage collection Processing logic of pool:

func poolCleanup(){
    // Discard the current victim and STW, so there is no need to lock
    for _, p := range oldPools {
        p.victim = nil
        p.victimSize = 0
    }
    
    // Copy local to victim and set the original local to nil
    for _, p := allPools {
        p.victim = p.local
        p.victimSize = p.localSize
        p.local = nil
        p.localSize = 0
    }
    
    oldPools, allPools = allPools, nil
}

In this code, you need to pay attention to the local field, because all the main free and available elements are stored in the local field to find the available elements. The local field contains a poolLocallnternal field and provides CPU cache alignment to avoid false sharing.

poolLocallnternal contains two fields: private and shared.

  • private, which represents a cached element and can only be accessed by a corresponding P. Because a P can only execute one goroutine at the same time, there will be no concurrency problem.
  • shared can be accessed by any p, but only local P can pushHead/popHead. Other p can jpopTail, which is equivalent to only one local P as Producer and multiple P as consumers. It is implemented using a local free queue list.

Get method

func (p *Pool) Get() interface{} {
    // Fix the current goroutine on the current P
    l, pid := p.pin()
    x := .private // Take priority from the private field of local to quickly
    l.private = nil
    if x == nil {
        // From the current local A pop-up appears in shared. Note that it is read from the head and removed
        x, _ = l.shared.popHead()
        if x == nil { // If not, steal one
            x = p.getSlow(pid)
        }
    }
    runtime_procUnpin()
    // If not, try to generate a New one using the New function
    if x == nil && p.New != nil {
        x = p.New()
    }
    return x
}

Get available elements from the local private field. Because there is no lock, the process of obtaining elements will be very fast. If there is no lock, try to obtain one from the local shared. If not, use the getSlow method to "steal" one from other shared. Finally, if not, try to create a New one using the New function.

getSlow method, indicating that it may take a long time. First, you need to traverse all the local es and try to take out an element from their shared. If you haven't found one yet, start stacking victim.

The logic of querying available elements in vintim is the same. First, search from the private of the corresponding victim. If not, search from the shared of other victims.

getSlow source logic:

func (p *Pool) getSlow(pid int) interface{} {
   // See the comment in pin regarding ordering of the loads.
   size := atomic.LoadUintptr(&p.localSize) // load-acquire
   locals := p.local                        // load-consume
   // Try to steal one element from other procs.
   for i := 0; i < int(size); i++ {
      l := indexLocal(locals, (pid+i+1)%int(size))
      if x, _ := l.shared.popTail(); x != nil {
         return x
      }
   }

   // Try the victim cache. We do this after attempting to steal
   // from all primary caches because we want objects in the
   // victim cache to age out if at all possible.
   size = atomic.LoadUintptr(&p.victimSize)
   if uintptr(pid) >= size {
      return nil
   }
   locals = p.victim
   l := indexLocal(locals, pid)
   if x := l.private; x != nil {
      l.private = nil
      return x
   }
   for i := 0; i < int(size); i++ {
      l := indexLocal(locals, (pid+i)%int(size))
      if x, _ := l.shared.popTail(); x != nil {
         return x
      }
   }

   // Mark the victim cache as empty for future gets don't bother
   // with it.
   atomic.StoreUintptr(&p.victimSize, 0)

   return nil
}

Put method

func (p *Pool) Put(x interface{}) {
    if x == nil { // nil values are discarded directly
        return
    }
    l, _ := p.pin()
    if l.private == nil { // If the local private does not have a value, you can set this value directly
        l.private = x
        x = nil
    }
    if x != nil { // Otherwise, join the local queue
        l.shared.pushHead(x)
    }
    runtime_procUnpin()
}

The logic of Put is relatively simple. Set local private first. If the private field already has a value, push this element to the local queue.

summary

What is Pool shared this time? How to use it, and learned about sync The data structure, execution process and implementation process of Pool. I believe I have learned the basic usage and understanding of Pool. If you want to know more, you can start with some pits of Pool, such as memory leakage, memory waste, etc. Later, I will write about GC of Go language and variable escape.

It's not easy to create. Give it a compliment!
If you need to see a collection later!
If you are interested in my article, please pay attention to it!
If the Milky way is concerned, click on the official account.

Topics: Go Concurrent Programming