SoFunction
Updated on 2025-03-05

How to ensure data thread safety without locking in Go language?

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 machineint64Variables of type will have intermediate states, and they will be split into two write operations (assembledMOVInstructions) - 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,GoThe solution given is, it allows us to not rely on unenforceable compatibilitytype, 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 variablecStore in oneType ofvinside.
  • c := ()- Read operations, thread-safe from memoryvRead 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 areinterface{}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 fromValueoutside,atomicA package is defined insideifaceWordsType, 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. CooperateCompareAndSwapUse it to achieve the optimistic locking effect.
  • passLoadPointerThis atomic operation is currentlyValueThe type stored in. The following is divided into three situations according to this type.
  • First write

    oneAfter the instance is initialized, itstypanddataThe field will be set to the zero value of the pointer nil, so first determine iftypWhether it is nil, if so, it proves thisValueThe 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) UseCASOperation, try totypSet 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 itvSet to the new value passed in. Note, here is written firstdataField, then writetypField. Because we aretypThe 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 seetypField 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 todataField.

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 currenttypIt'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 seetypanddataConstruct 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 controlMutexThe 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!