Learn the memory model of Go through Once

Once is an object that will perform exactly one action. Once is an object that ensures that an action is performed only Once. The most typical scenario is the singleton mode.

The singleton pattern

package main

import (
	"fmt"
	"sync"
)

type Instance struct {
	name string
}

func (i Instance) print() {
	fmt.Println(i.name)
}

var instance Instance

func makeInstance() {
	instance = Instance{"go"}
}

func main() {
	var once sync.Once
	once.Do(makeInstance)
	instance.print()
}
Copy the code

The function in once.Do is executed only once, and is guaranteed that by the time once.Do returns, the function passed to Do has completed its execution. (If multiple Goroutines execute once.Do at the same time, it can ensure that the goroutine that preempted the right to execute once.

The source code

The source code is very simple, but so simple less than 20 lines of code can learn a lot of knowledge, very strong.

package sync

import (
	"sync/atomic"
)

type Once struct {
	done uint32
	m    Mutex
}

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 {
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}
Copy the code

Here are a few key points:

  1. The Do method should use atomic.LoadUint32(&O.tone) == 0 instead of o.tone == 0
  2. Why is O.tone == 0 used directly in the doSlow method
  3. Atomic.StoreUint32(&o.tone, 1)

Answer the first question first? If o.tone == 0, doSlow’s o.tone setting will not be observed in time. For specific reasons, you can refer to the memory model of Go, as mentioned in the article:

Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.

To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages.
Copy the code

When a variable is visited by more than one gorouting, it must be kept in order. This can be done using the sync or sync/atomic package. The LoadUint32 function ensures that doSlow can be fetched in a timely manner after o.tone is set.

O.tone == 0 can be used directly because Mutex is used for the lock operation. O.tone == 0 is in the critical region of the lock operation, so it can be directly compared.

Atomic.StoreUint32(&O.tone, 1) is also in the critical section. Why not assign the value to o.tone = 1? It really comes down to memory mode. Mutex can only guarantee that operations in the critical region are observable — that is, only the code between O.m.lock () and defer o.m.lock () is observable for the value of O.Tone. Therefore, the StoreUint32 should be used to ensure atomicity.

Here is not found harvest a lot, and more powerful. Let’s see why dong uses uint32 instead of uint8 or bool.

type Once struct {
    // done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/x86),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}
Copy the code

The LoadUint8 and LoadBool operations are not available in the atomic package.

And then see the comments, we found that the more esoteric secret: comments referred to the concept of an important hot path, namely Do method invocation is high frequency, and each call to access the done, done in the structure of the first field, can be accessed directly by structure pointer (access to other fields need to be calculated by the offset is slow)