cover

The Principle of Go sync.Once

sorcererxwβ€’

sync.Once is used to ensure a task is executed exactly once. Its implementation is very simple:

type Once struct {
	done uint32
	m    Mutex
}

func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 {
		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()
	}
}

We can see the workflow of Once:

  • Use atomic to check done to determine whether it has been executed.
  • Ensure that only one goroutine enters execution at the same time through Mutex.
  • Check whether done has been rewritten.
  • Execute and update the done value

The entire process is actually an implementation of Double-checked locking singleton. It introduces double check on the basis of the lock, so there are a few points worth noting:

  • Why set multiple checks?

    Take a look at doSlow. This is a complete and independent workflow in itself, but the problem is that it's too slow, and every check requires lock contention. Therefore, in order to improve performance, a preliminary check with atomic is used to quickly determine whether the process has been completed.

  • Since we have already checked the value of done before, why do we need to check it again before acquiring the lock to execute?

    Because the entire process is not an atomic operation, while waiting for the mutex lock, the previous goroutine may have already rewritten the value of done. The goroutines that are allowed to enter later need to ensure that done has not been rewritten.