1. Introduction
This article will introduce the Go languageConcurrent primitives, including
Basic usage methods, precautions, etc. Better to use
To reduce the repetitive creation of objects, maximize the reuse of objects, reduce the pressure on program GC, and improve the performance of programs.
2. Problem introduction
2.1 Problem Description
Here we implement a simple JSON serializer, which can implement amap[string]int
Serialize to a JSON string, and implement it as follows:
func IntToStringMap(m map[string]int) (string, error) { // Define one for cache data var buf ([]byte("{")) for k, v := range m { ((`"%s":%d,`, k, v)) } if len(m) > 0 { (() - 1) // Remove the last comma } ([]byte("}")) return (), nil }
Used hereto cache the data, and then follow
key:value
In the form of , the data is generated by a string and then returned, the implementation is relatively simple.
Each callIntToStringMap
When a method is created, ato cache intermediate results, and
In fact, it can be reused because the serialization rules have little to do with them, they are just used as a cache area.
But the current implementation is every callIntToStringMap
When a,If in an application, the request concurrency is very high, it is frequently created and destroyed
It will bring great performance overhead, which will lead to frequent allocation of objects and garbage collection, increasing the pressure of memory usage and garbage collection.
So what's the way to makeCan it be reused to the greatest extent and avoid repeated creation and recycling?
2.2 Solution
In fact, we can find that in order toCan be reused to avoid repeated creation and recycling, we only need to
Cache, and when needed, remove it from the cache; when used up, put it back into the cache pool. This way, there is no need for every call
IntToStringMap
When a method is created,。
Here we can implement a cache pool ourselves. When objects are needed, they can be retrieved from the cache pool. When objects are not needed, they can be put back into the cache pool.IntToStringMap
Methods requireWhen , it is taken from the cache pool. When used up, it is put back into the cache pool and wait for the next acquisition. Here is a slicing implementation
Cache pool.
type BytesBufferPool struct { mu pool []* } func (p *BytesBufferPool) Get() * { () defer () n := len() if n == 0 { // When there is no object in the cache pool, create a return &{} } // When there is an object, take out the last element of the slice and return v := [n-1] [n-1] = nil = [:n-1] return v } func (p *BytesBufferPool) Put(buffer *) { if buffer == nil { return } // Put it into slices () defer () () = append(, buffer) }
aboveBytesBufferPool
Achievedcache pool, where
Get
Methods are used to retrieve objects from the cache pool. If there is no object, create a new object to return;Put
Method is used to replace objectsBytesBufferPool
Among them, useBytesBufferPool
To optimizeIntToStringMap
。
// First define a BytesBufferPoolvar buffers BytesBufferPool func IntToStringMap(m map[string]int) (string, error) { // No longer create it yourself, but remove it from BytesBufferPool buf := () // After the function is finished, it will be put back into the cache pool defer (buf) ([]byte("{")) for k, v := range m { ((`"%s":%d,`, k, v)) } if len(m) > 0 { (() - 1) // Remove the last comma } ([]byte("}")) return (), nil }
At this point, we implemented a cache pool by ourselves, and successfullyInitToStringMap
Functions have been optimized, reducingFrequent creation and recycling of objects has improved the frequent creation and recycling of objects to a certain extent.
but,BytesBufferPool
There are actually several problems with the implementation of this cache pool. First, it can only be used for cacheObject; Second, the number of cached objects in the object pool cannot be dynamically adjusted according to the actual situation of the system. If the concurrency is high during a certain period of time,
Objects are created in large quantities and put back after use
BytesBufferPool
After that, it will never be recycled, which may lead to memory waste, and a little more severely, it will also lead to memory leakage.
Since there are these problems with custom cache pools, we can't help but ask, is there a more convenient way in the Go language standard library to help us cache objects?
Not to mention, there is really, the Go standard library provides, can be used to cache objects that need to be created and destroyed frequently, and it supports cache of any type of objects, and at the same time
It is possible to adjust the number of objects in the cache pool according to the actual situation of the system. If an object has not been used for a long time, it will be recycled at this time.
Compared to the buffer pool that you implement,It has higher performance, makes full use of the capabilities of multi-core CPUs, and can also dynamically adjust the number of objects in the buffer pool according to the load of the currently used objects of the system. It is also relatively simple to use. It can be said that it is the best choice for realizing a stateless object cache pool.
Let's take a look belowBasic usage of the
IntToStringMap
The method is being implemented.
3. Basic use
3.1 How to use
Basic definition of 3.1.1
The definition is as follows:
Get
,Put
Two methods:
type Pool struct { noCopy noCopy local // local fixed-size per-P pool, actual type is [P]poolLocal localSize uintptr // size of the local array victim // local from previous cycle victimSize uintptr // size of victims array New func() any } func (p *Pool) Put(x any) {} func (p *Pool) Get() any {}
-
Get
Method: FromRemove cached objects
-
Put
Method: Put the cache object intoamong
-
New
Function: CreatingWhen a
New
function, whenGet
When the method cannot obtain the object, it will be called at this timeNew
The function creates a new object and returns it.
3.1.2 How to use
When usingWhen you are in the process of , the following steps are usually required:
- Use first
Define an object buffer pool
- When using the object, remove it from the buffer pool
- After use, put the object back into the buffer pool again
Here is a simple code example showing how to use itProbably the code structure:
type struct data{ // Define some properties} //1. Create a cache pool for data objectsvar dataPool = {New: func() interface{} { return &data{} }} func Operation_A(){ // 2. Where data objects are needed, take them out from the cache pool d := ().(*data) // Perform subsequent operations // 3. Replace the object into the cache pool (d) }
3.2 Use examples
Let's useCome right
IntToStringMap
Carry out transformation to achieveThe reuse of objects can also automatically adjust the number of objects in the buffer pool according to the current status of the system.
// 1. Define an object buffer poolvar buffers = { New: func() interface{} { return &{} }, } func IntToStringMap(m map[string]int) (string, error) { // 2. When needed, take an object out of the buffer pool buf := ().(*) () // 3. After using it, put it back into the buffer pool defer (buf) ([]byte("{")) for k, v := range m { ((`"%s":%d,`, k, v)) } if len(m) > 0 { (() - 1) // Remove the last comma } ([]byte("}")) return (), nil }
We useAchieved
The buffer pool, in
IntToStringMap
In the function, webuffers
Get one fromObject and put it back into the pool at the end of the function, avoiding frequent creation and destruction
Object overhead.
At the same time,exist
IntToStringMap
If calls are not frequent, they can be automatically recycledIn-house
Objects can reduce memory pressure without user concern. Moreover, its underlying implementation also takes into account the concurrent execution of multi-core CPUs. Each processor will have its corresponding local cache, which also reduces the overhead of multi-threaded locking to a certain extent.
As can be seen from the above,It is very simple to use, but there are still some precautions. If used improperly, it may still lead to memory leakage and other problems. Let's introduce it below
Things to note when using it.
4. Precautions for use
4.1 Pay attention to the size of the object you put in
If you don't pay attention to put it inThe size of the object in the buffer pool may appear
There are only a few objects in it, but it occupies a large amount of memory, resulting in memory leaks.
Here, for objects of fixed size, you don't need to pay too much attention to putting them in.The size of the object in this scenario is very small. However, if put in
There is a mechanism for automatic expansion of objects in it. If you do not pay attention to it,
The size of the object in this case will likely cause memory leakage. Let’s see an example below:
func Sprintf(format string, a ...any) string { p := newPrinter() (format, a) s := string() () return s }
Sprintf
The method completes assembly based on the passed format and the corresponding parameters and returns the corresponding string result. According to the normal idea, you only need to apply for onebyte
Array, and then according to certain rules,format
andparameter
Put the contents inbyte
In the array, thebyte
Convert the array to a string and return it.
According to the above idea, we found that in fact, every time we use itbyte
Arrays are reusable and do not require repeated construction.
In factSprintf
The same is true for the implementation of the method.byte
Arrays do not actually create a new one at a time, but reuse them. It achieves app
Structure,format
andparameter
The responsibility of assembling into strings according to certain rules is delivered topp
Structure, at the same timebyte
Array aspp
Member variables of the structure.
Thenpp
Put an instance ofAmong them, realize
pp
Reuse purpose, thus introducing the avoidance of duplicate creationbyte
Arrays lead to frequent GCs, and also improve performance. Below isnewPrinter
The logic of the method, obtainpp
Structures, all fromGet it in:
var ppFree = { New: func() any { return new(pp) }, } // newPrinter allocates a new pp struct or grabs a cached one. func newPrinter() *pp { // Get pp from ppFree p := ().(*pp) // Execute some initialization logic = false = false = false (&) return p }
Return to the abovebyte
array, at this time it acts aspp
A member variable of the structure, used for intermediate results of string formatting, is defined as follows:
// Use simple []byte instead of to avoid large dependency. type buffer []byte type pp struct { buf buffer // Omit other unrelated fields}
There seems to be no problem here, but in fact, there may be problems such as memory waste or even memory leaks. If this happens, there is a very long string that needs to be formatted, and then it is calledSprintf
to implement formatting, at this timepp
In the structurebuffer
It also needs to be continuously expanded until the entire string length can be stored.pp
In the structurebuffer
It will occupy a relatively large memory.
whenSprintf
After the method is completed, re-set thepp
Structure placementAmong them, at this time
pp
In the structurebuffer
The occupied memory will not be released.
However, if called next timeSprintf
Method to format the string, the length is not that long, but at this timeTaken out
pp
In the structurebyte array
The length is after the last expansionbyte array
, at this time, memory waste will be caused, and in serious cases, it may even lead to memory leakage.
Therefore, becausepp
In the objectbuffer
The memory occupied by the field will be automatically expanded, and the size of the object is not fixed, sopp
Replace the objectWhen in the middle, you need to pay attention to the size of the object. If it is too large, it may lead to memory leakage or memory waste. You can directly discard it and not re-place it.
among. In fact,
pp
Replace the structureIt is also based on this logic, and it will first judge
pp
In structurebuffer
If the memory occupied by the field is too large, it will not be re-placed at this time.Among them, they are discarded directly, as follows:
func (p *pp) free() { // If the size of the byte array exceeds a certain limit, it will be returned directly if cap() > 64<<10 { return } = [:0] = nil = {} = nil // Otherwise, it will be put back in (p) }
Based on the above summary, ifIf the memory size occupied by the stored objects is not fixed, you need to pay attention to the size of the object being placed to prevent memory leakage or memory waste.
4.2 Do not put database connection/TCP connection into it
The acquisition and release of resources such as TCP connections and database connections usually require certain specifications, such as explicitly closing the connection after the connection is completed. These specifications are formulated based on network protocols, database protocols and other specifications. If these specifications are not correctly followed, it may lead to problems such as connection leakage and exhaustion of connection pool resources.
When usingWhen storing connection objects, if these connection objects are not explicitly closed, they will remain in memory until the process ends. If there are too many connection objects, these unclosed connection objects will occupy too much memory resources, resulting in memory leaks and other problems.
For example, suppose there is an objectConn
Indicates a database connection, itsClose
Method is used to close the connection. IfConn
Put the object inand after being removed from the pool and used it, no manual call is called
Close
If the method returns the object, these connections will remain open until the program exits or reaches the connection limit. This can lead to resource exhaustion or some other problem.
Here is a simple example code usingStore TCP connection objects, demonstrating the leakage of connection objects:
import ( "fmt" "net" "sync" "time" ) var pool = &{ New: func() interface{} { conn, err := ("tcp", "localhost:8000") if err != nil { panic(err) } return conn }, } func main() { // Simulate the connection for i := 0; i < 100; i++ { conn := ().() (100 * ) (conn, "GET / HTTP/1.0\r\n\r\n") // Don't close the connection // When you are not using a connection, just release the connection object to the pool (conn) } }
In the above code, we useCreated a TCP connection and stored it to
middle. When emulating a connection using, we get the connection object from the pool, send a simple HTTP request to the server, and then release the connection object into the pool. However, we do not explicitly close the connection object. If the number of connected objects is large, these unclosed connected objects will occupy a large amount of memory resources, resulting in memory leaks and other problems.
Therefore, certain specifications should be followed for the release of resources such as database connections or TCP connections, and should not be used at this time.To reuse, you can implement database connection pooling and other methods to achieve connection multiplexing.
5. Summary
This article introduces the Go languagePrimitive, it is a very good tool to realize object reuse, reduce program GC frequency, and improve program performance.
We first introduced a scenario that requires reusing objects through a simple JSON serializer implementation, and then implemented a buffer pool ourselves. From the problems existing in the buffer pool, we then led to the. Next, we introduce
Basic use of and application of it to the implementation of JSON serializer.
Next, the introductionCommon precautions, if you need to pay attention to putting them in
The size of the object is analyzed, thus telling the
Some possible precautions can help you better use them.
The above is a detailed understanding of the use of Go. For more information about Go, please follow my other related articles!