introduction
Many people may not have noticed that in Go (even in most languages), an ordinary assignment statement is not actually an atomic operation. For example, write on a 32-bit machineint64
Variables of type will have intermediate states, and they will be split into two write operations (assembledMOV
Instructions) - Write low 32 bits and write high 32 bits. Assign int64 on machine 32
If a thread has just finished writing the lower 32 bits and has not had time to write the higher 32 bits, and another thread reads this variable, then it gets an illogical intermediate variable, which is likely to cause bugs in our program.
This is just a basic type. If we assign values to a structure, the probability of it having concurrency problems is even higher. It is very likely that the thread has just finished writing a small part of the fields and the thread reads the variable, so that it can only read a part of the value that has been modified. This obviously destroys the integrity of the variable, and the read value is completely wrong.
Faced with this problem of reading and writing variables under multithreading,Go
The solution given is, it allows us to not rely on unenforceable compatibility
type, and can also encapsulate read and write operations of any data type into atomic operations.
How to use
Type provides two reading and writing methods:
-
(c)
- Write operation, to convert the original variablec
Store in oneType of
v
inside. -
c := ()
- Read operations, thread-safe from memoryv
Read the content stored in the previous step.
Here is a simple example demonstrationHow to use.
type Rectangle struct { length int width int } var rect func update(width, length int) { rectLocal := new(Rectangle) = width = length (rectLocal) } func main() { wg := {} (10) // 10 coroutines are updated concurrently for i := 0; i < 10; i++ { go func(i int) { defer () update(i, i+5) }(i) } () r := ().(*Rectangle) ("=%d\=%d\n", , ) }
You may be curious whyWithout locking, thread safety guarantees for read and write variables are provided. Next, let’s take a look at its internal implementation.
Internal implementation
It is designed to store any type of data, so its internal fields are
interface{}
type.
// A Value provides an atomic load and store of a consistently typed value. // The zero value for a Value returns nil from Load. // Once Store has been called, a Value must not be copied. // // A Value must not be copied after first use. type Value struct { v interface{} }
Apart fromValue
outside,atomic
A package is defined insideifaceWords
Type, this is actuallyinterface{}
The internal representation (), which functions tointerface{}
Type decomposition to get its original type (typ) and true value (data).
// ifaceWords is interface{} internal representation. type ifaceWords struct { typ data }
Writing thread safety guarantee
Look at the code directly
// Store sets the value of the Value to x. // All calls to Store for a given Value must use values of the same concrete type. // Store of an inconsistent type panics, as does Store(nil). func (v *Value) Store(val interface{}) { if val == nil { panic("sync/atomic: store of nil value into Value") } // Convert the existing (v) and the value (val) to the ifeWords type respectively. // In this way, we can get the original type (typ) and the real value (data) of these two interface{} next step. vp := (*ifaceWords)((v)) vlp := (*ifaceWords)((&val)) for { typ := LoadPointer(&) if typ == nil { // Attempt to start first store. // Disable preemption so that other goroutines can use // active spin wait to wait for completion; and so that // GC does not see the fake type accidentally. runtime_procPin() if !CompareAndSwapPointer(&, nil, (^uintptr(0))) { runtime_procUnpin() continue } // Complete first store. StorePointer(&, ) StorePointer(&, ) runtime_procUnpin() return } if uintptr(typ) == ^uintptr(0) { // First store in progress. Wait. // Since we disable preemption around the first store, // we can wait with active spinning. continue } // First store completed. Check type and overwrite data. if typ != { panic("sync/atomic: store of inconsistently typed value into Value") } StorePointer(&, ) return } }
Probably the logic:
- The beginning is an infinite for loop. Cooperate
CompareAndSwap
Use it to achieve the optimistic locking effect. - pass
LoadPointer
This atomic operation is currentlyValue
The type stored in. The following is divided into three situations according to this type.
-
First write
one
After the instance is initialized, its
typ
anddata
The field will be set to the zero value of the pointer nil, so first determine iftyp
Whether it is nil, if so, it proves thisValue
The instance has not been written to the data. Then there is an initial write operation: -
runtime_procPin()
This is a function in runtime. On the one hand, it prohibits the scheduler from preemption of the current goroutine, so that it is not interrupted by other goroutines when executing the current logic, so that the work can be completed as soon as possible. On the other hand, during the prohibited preemption, GC threads cannot be enabled, which prevents GC threads from seeing an inexplicable pointer.^uintptr(0)
type (this is the intermediate state during assignment).1) Use
CAS
Operation, try totyp
Set as^uintptr(0)
This intermediate state. If it fails, it proves that another thread has already completed the assignment operation first, then it will unlock the preemption lock and then return to the first step of the for loop.2) If the setting is successful, it proves that the current thread has grabbed this "optimistic lock", and it can safely put it
v
Set to the new value passed in. Note, here is written firstdata
Field, then writetyp
Field. Because we aretyp
The value of the field is used as the basis for determining whether the write is completed or not. -
The first write has not been completed
If you see
typ
Field or^uintptr(0)
This intermediate type proves that the first write just now has not been completed, so it will continue to loop until the first write is completed. -
The first write has been completed
First check whether the type of the last write is consistent with the type to be written this time, and if it is inconsistent, an exception will be thrown. On the contrary, directly write the value to be written this time to
data
Field.
The main idea of this logic is that in order to complete the atomic writing of multiple fields, we can grasp one of the fields and use its state to mark the state of the entire atomic writing.
Read (Load) operation
First upload the code:
// Load returns the value set by the most recent Store. // It returns nil if there has been no call to Store for this Value. func (v *Value) Load() (val interface{}) { vp := (*ifaceWords)((v)) typ := LoadPointer(&) if typ == nil || uintptr(typ) == ^uintptr(0) { // First store not yet completed. return nil } data := LoadPointer(&) vlp := (*ifaceWords)((&val)) = typ = data return }
Reading is relatively simple, it has two branches:
- If the current
typ
It's nil or^uintptr(0)
, it proves that the first write has not started or has not been completed, so it will directly return nil (no intermediate state is exposed to the outside world). - Otherwise, according to what you see
typ
anddata
Construct a newinterface{}
Go back out.
Summarize
This article is introduced from the bottom of the bottomuse posture, and internal implementation. In addition, the atomic operation isLow-level hardwareSupport, for the protection of a variable update, atomic operations are usually more efficient and can take advantage of the advantages of computer multi-core. If the update is a composite object, it should be used.
Encapsulated implementation.
We often use concurrent synchronization controlMutex
The lock is from the operating systemSchedulerImplementation, locks should be used to protect a piece of logic.
The above is how to ensure data thread safety without locking in Go language? Details of the content, more about how to ensure data thread safety without locking in Go language? Please follow my other related articles for information!