More dry goods, public concern: Qiya cloud storage
Antecedents feed
Last time, we combed and analyzed the usage level and got the following tips:
- Sync.pool is designed to increase the reuse rate of temporary objects and reduce GC burden.
- Do not anticipate objects from pool. Get. They may be new or old (previously used and then Put in).
- You can’t make assumptions about the number of elements in the Pool, you can’t;
- Sync. Pool’s own Get and Put calls are concurrency safe,
sync.New
Pointer to the initialization function will be called concurrently, inside the security of only their own know; - When you run out of an instance fetched from a Pool, be sure to call Put or the Pool won’t be able to reuse the instance. Normally this is done by defer;
Official opening statement:
A Pool is a set of temporary objects that may be individually saved and retrieved.
The principle of analyzing
Let’s take a closer look at sync.pool in terms of data structure and implementation logic.
The data structure
The Pool structure
The Sturct Pool structure is a structure for use by users. It is defined as:
type Pool struct {
// Check whether the Pool is copied because the Pool does not want to be copied.
// Once you have this field, you can use the Go Vet tool to detect problems at compile time;
noCopy noCopy
// Array structure, corresponding to each P, the number is the same as the number of P;
local unsafe.Pointer
localSize uintptr
// Victim and victimSize will victimize local and localSize, respectively;
// victim's purpose is to reduce performance jitter caused by cold start after GC and make allocation objects smoother;
victim unsafe.Pointer
victimSize uintptr
// The object initializes the constructor, using the method definition
New func(a) interface{}}Copy the code
A few caveats:
- NoCopy to prevent copy from piling code, but this does not prevent compilation, only through
go vet
Check out; local
和localSize
These two fields implement an array whose elements arepoolLocal
Structure for managing temporary objects;victim
和victimSize
This is inpoolCleanup
It’s assigned in the flow, and what the assignment islocal
和localSize
. The victim mechanism changes the Pool clearing from one GC round to two GC rounds to improve object reuse rate and reduce jitter.- Users can only assign New fields to define the object initialization construction behavior;
PoolLocal structure
This structure is the key structure for managing cache elements in a Pool. Pool. Local refers to an array of this type.
The array of structures in the Pool is allocated according to the number of P’s, and each P corresponds to one of these structures.
// Pool.local Specifies the type of the array element pointed to
type poolLocal struct {
poolLocalInternal
// Populate poolLocal to 128-byte alignment to avoid false Sharing performance issues
pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
// Manage the internal structure of the cache, corresponding to each P, no lock operation
type poolLocalInternal struct {
// Each P is private and does not require locking
private interface{}
// A double-linked list structure for attaching cache elements
shared poolChain
}
Copy the code
poolChain
We can take a look at the poolChain structure for a moment. This is purely a join, with only two Pointers, occupying 16 bytes of memory.
type poolChain struct {
head *poolChainElt
tail *poolChainElt
}
Copy the code
So the key is the list element, which is poolChainElt, which looks like this:
type poolChainElt struct {
// Is essentially an array memory space, managed in ringbuffer mode;
poolDequeue
// List pointer
next, prev *poolChainElt
}
type poolDequeue struct {
headTail uint64
// vals is a ring buffer of interface{} values stored in this
// dequeue. The size of this must be a power of 2.
vals []eface
}
Copy the code
PoolChainElt is an element point in a list that contains an array space (similar to ringBuffer), and the pool-managed cache objects are stored in poolDequeue (VALS []).
Get
func (p *Pool) Get(a) interface{} {
// lock G on M (declare M cannot be preempted), return the ID of P bound to M
// In the current scenario, it can also be considered that G is bound to P, because P cannot be preempted in this scenario, only P is preempted in system calls;
l, pid := p.pin()
// If the cached element can be fetched from private, it will be the fastest path;
x := l.private
l.private = nil
if x == nil {
// Get from the shared queue, Get from the shared queue, and post from the Put queue;
x, _ = l.shared.popHead()
if x == nil {
// Try to fetch elements from the queue for other P's, or from the victim cache
x = p.getSlow(pid)
}
}
// the g-m lock is removed
runtime_procUnpin()
// The slowest path: field initialization, where there is no object in the Pool, can only be created on site;
if x == nil&& p.New ! =nil {
x = p.New()
}
// Return the object
return x
}
Copy the code
The semantics of Get is to fetch an element from the Pool. The key here is that the element is cached in layers, from the fastest to the slowest. The fastest is to directly fetch the private field in the corresponding list of P, and the slowest is to call the New function to construct on the spot.
Try path:
- The currentThe corresponding P
local.private
Field; - The currentThe corresponding P
local
Bidirectional linked list; - otherThe corresponding P
local
List; - Element in the victim cache;
New
Field structure;
runtime_procPin
Runtime_procPin is a layer encapsulation of procPin, which is implemented as follows:
func procPin(a) int {
_g_ := getg()
mp := _g_.m
mp.locks++
return int(mp.p.ptr().id)
}
Copy the code
The purpose of the procPin function is for G to be preempted (that is, G will not be running on M). The core implementation here is the mp.lock ++ operation, which is evaluated in newStack if
if preempt {
// The preemption flag has been set, but the execution right can be given only if the conditions are met;
ifthisg.m.locks ! =0|| thisg.m.mallocing ! =0|| thisg.m.preemptoff ! =""|| thisg.m.p.ptr().status ! = _Prunning { gp.stackguard0 = gp.stack.lo + _StackGuard gogo(&gp.sched)// never return}}Copy the code
Pool.pinSlow
It is important to mention that this function does something very important, which is usually only entered when the Pool calls Get for the first time (note that this is the first Get call for each P, but only G on P can do this because of the allPoolsMu lock mutex).
func (p *Pool) pinSlow(a) (*poolLocal, int) {
// g-m is unlocked first
runtime_procUnpin()
// The following logic is in the global lock allPoolsMu
allPoolsMu.Lock()
defer allPoolsMu.Unlock()
// Get the id of the current g-m-p, P
pid := runtime_procPin()
s := p.localSize
l := p.local
if uintptr(pid) < s {
return indexLocal(l, pid), pid
}
if p.local == nil {
For the first time, pools need to register themselves in the allPools array
allPools = append(allPools, p)
}
// Number of P's
size := runtime.GOMAXPROCS(0)
// The size of the local array is runtime.GOMAXPROCS(0)
local := make([]poolLocal, size)
atomic.StorePointer(&p.local, unsafe.Pointer(&local[0])) // store-release
atomic.StoreUintptr(&p.localSize, uintptr(size)) // store-release
return &local[pid], pid
}
Copy the code
PinSlow does the following things:
- For the first time
Pool
You need to register yourselfallPools
The array; Pool.local
According to the arrayruntime.GOMAXPROCS(0)
If this is the default, then this is the number of P, which is the number of cpus;
runtime_procUnpin
This is the function corresponding to runtime_procPin, which declares that M can be preempted, with the field M.locks –.
func procUnpin(a) {
_g_ := getg()
_g_.m.locks--
}
Copy the code
Put
The Put method is very simple, because it’s post-processing, and the cleanup is done in the background at runtime, so it’s just putting the element in the queue.
// Put an element into the pool;
func (p *Pool) Put(x interface{}) {
if x == nil {
return
}
/ / G - M lock
l, _ := p.pin()
if l.private == nil {
// Try to place it in the fastest position, which also corresponds to the order of Get requests;
l.private = x
x = nil
}
ifx ! =nil {
// Put it in the bidirectional list
l.shared.pushHead(x)
}
// the g-m lock is removed
runtime_procUnpin()
}
Copy the code
However, there is a small point to note that even Put calls p.pin(), so pool.local may also be created here.
runtime
The global variable
Each Pool structure is added to the global queue. In the SRC /sync/pool.go file, several global variables are defined:
var (
/ / the mutex
allPoolsMu Mutex
// Pool array, all pools have registered addresses;
allPools []*Pool
// victim mechanism;
oldPools []*Pool
)
Copy the code
The background process
init
Register the cleanup function at initialization time.
func init(a) {
runtime_registerPoolCleanup(poolCleanup)
}
Copy the code
The poolCleanup function is called when gcStart calls the ClearPools () function at the start of the Golang GC. In other words, each GC round does a cleanup of all the pools.
poolCleanup
This is done periodically, registered with sync Package init, and executed by the Runtime in the background, to batch clean allPools elements.
func poolCleanup(a) {
// Clean up the element victim on oldPools
for _, p := range oldPools {
p.victim = nil
p.victimSize = 0
}
// Migrate local cache to victim;
// There is no need for GC to empty all the Pool, and there is a victim below the bottom.
for _, p := range allPools {
p.victim = p.local
p.victimSize = p.localSize
p.local = nil
p.localSize = 0
}
// Clear a wave of allPools
oldPools, allPools = allPools, nil
}
Copy the code
Victim has changed the recovery action from one to two to be more resistant. Only the previous cache object is actually cleared each time. The current cache object is only moved to the recycle bin (victim).
Knowledge summary:
- This is called at the beginning of each GC round
poolCleanup
Functions; - Using a two-round cleanup process, the local cache and victim cache work together to protect against fluctuations.
Question to consider
The principle above has been dissected very clearly, now let’s consider some different questions:
1. What happens if Put is called instead of pool. Get?
There are no exceptions. The Pool can accept objects of any type from any source. You can call pool. Put even if it’s not pool. Get, and once you’ve done that, the Pool is no longer a single object element, but a grocery store.
Why?
- First of all,
Put(x interface{})
The interface does not make judgments or assertions about type X; - Second,
Pool.Put
There is no internal assertion or determination of the type, and there is no way to determine whether the element is from the interface of Get.
So, in the last post on Pool postures, after calling the pool. Get element, I had a line of type assertions that said something like this:
buffer := bufferPool.Get()
_ = buffer.(*[]byte)
Copy the code
This is important to note because the Sync.pool framework supports storing any type, which can essentially be a grocery store, so the types of objects that Get out and Put in are under the control of the business itself.
2. Pool.Get
Out of the object, whyPool.Put
Put back into the Pool Pool, in order not to become their own hate garbage?
First, pool. Get and pool. Put must be used together in terms of usage posture, and usually defer pool. Put is used to ensure that elements are released into the Pool.
Have you ever thought about why you suggest Get and Put go together? Will it become unrecyclable garbage if it is not matched?
Pool.get, pool.put, pool.put, pool.put, pool.get, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put, pool.put GC will release him.
As an extreme example, what happens if I make a change in the Pool posture, pool.get every time, and never call pool.put?
The answer is: nothing happens, the program runs as usual. Every time a Pool gets, a New function is executed to construct an object, and the Pool loses its essential function: reuse temporary objects. The very purpose of calling pool.put is to reuse objects.
3. Can the Pool itself be used after replication?
No, but you can do it. What do you mean?
If you copy a Pool in your code, your go Build will compile well, but it may cause internal leaks. The implementation of struct Pool explicitly states that copy is not allowed. Here is the official quote:
// A Pool must not be copied after first use.
Struct Pool has a Pool. NoCopy field that explicitly restricts you from copying, but this can only be checked by running Go Vet (so you must go VET static check before compiling your code to avoid a lot of problems).
$:~/pool$ go vet test_pool.go
# command-line-arguments
./test_pool.go:26:20: assignment copies lock value to bufferPool2: sync.Pool contains sync.noCopy
Copy the code
Why Pool copy?
After the Copy, we have two reference sources for the objects in the same Pool. After the original Pool is empty, the object in the Copy Pool is not cleared, so that all the objects in the Pool are leaked. And the basis of lock-free design in a Pool is that multiple Goroutines will not operate on the same data structure, which cannot be guaranteed after a Pool copy. Similar to sync.waitgroup, the sync.cond first field uses noCopy, so these two structures cannot be copied.
Therefore, do not copy Pool, be sure to go vet before compiling code.
conclusion
To sum up the above points:
- Pool is to improve the reuse rate of temporary objects.
- Pool uses a two-tier recycle policy (local + victim) to avoid performance fluctuations.
- Pool is essentially a grocery store property that can store anything. Service users can control what items are put in the Pool and what types of items are expected to be taken out of the Pool.
- Pool The cache object in a Pool is also layered. The cache object is accessed from the hottest data to the coldest data.
- The Pool is concurrency safe, but has no lock structure. The principle is that each P is allocated a cache array (
poolLocalInternal
Array), so that the cache structure does not cause concurrency; - Never copy a Pool. Explicitly forbid it. Otherwise, it will cause memory leaks and program concurrent logic errors.
- Used before the code is compiled
go vet
Do static check, can reduce a lot of problems; - The local array of the current Pool is passed to the victim array handle, and all the elements in the victim cache are cleared. In other words, after the victim mechanism is introduced, the object’s cache time becomes two GC cycles;
- Don’t make any assumptions about the objects in the Pool. There are two options: either return the memset object and then call it
Pool.Put
, orPool.Get
Remove the memset after use;
More dry goods, public concern: Qiya cloud storage