Изучите модель памяти Go с помощью Once
После официального описанияOnce is an object that will perform exactly one action, то есть Once — это объект, предоставляющий функцию, гарантирующую выполнение действия только один раз.Наиболее типичным сценарием является шаблон singleton.
одноэлементный шаблон
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()
}
Функция в Once.Do будет выполняться только один раз, и гарантируется, что при возврате Once.Do функция, переданная в Do, была выполнена. (Когда несколько горутин выполняются один раз.Do одновременно, можно гарантировать, что горутина, которая вытесняет право на выполнение Once.Do, выполняется один раз.Do до того, как другие горутины могут быть возвращены)
исходный код
Исходный код очень прост, но такой простой код, состоящий менее чем из 20 строк, действительно может дать много знаний, что очень важно.
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()
}
}
Вот несколько ключевых моментов познания:
- Почему метод Do не использует напрямую o.done == 0, а использует atomic.LoadUint32(& o.done) == 0
- Почему вы используете o.done == 0 непосредственно в методе doSlow
- Так как Блокировка была использована, почему бы не напрямую o.done = 1, а также нужно atomic.StoreUint32(&o.done, 1)
Сначала ответьте на первый вопрос? Если o.done == 0 напрямую, это сделает невозможным наблюдение за установкой значения o.done с помощью doSlow во времени. По конкретным причинам см.Модель памяти Go, упоминается в статье:
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.
Общая идея заключается в том, что когда к переменной обращаются несколько горутингов, они должны быть гарантированно упорядочены (синхронизированы), что может быть реализовано с помощью пакета sync или sync/atomic. Использование LoadUint32 может гарантировать, что doSlow может быть получен вовремя после установки o.done.
Глядя на второй вопрос, вы можете использовать o.done == 0 напрямую, потому что Mutex используется для операции блокировки, а o.done == 0 находится в критической секции операции блокировки, поэтому его можно сравнивать напрямую.
Полагаю, здесь вы зададите третий вопрос: atomic.StoreUint32(&o.done, 1) тоже находится в критической секции, почему бы не назначить его напрямую через o.done = 1? На самом деле это связано с режимом памяти.Мьютекс может гарантировать только то, что операции в критических секциях будут наблюдаться.То есть только код между o.m.Lock() и defer o.m.Unlock() можно наблюдать до значения o.done. Тогда это ненаблюдаемая ситуация при доступе к o.done в Do, поэтому для обеспечения атомарности необходимо использовать StoreUint32.
Вы обнаружили, что вы много приобрели здесь, и есть еще более сильные. Давайте посмотрим, почему dong не использует uint8 или bool, а использует uint32?
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
}
Причину можно увидеть на данный момент: операции LoadUint8 и LoadBool не предусмотрены в пакете atomic.
Затем, просматривая аннотации, мы обнаруживаем более эзотерический секрет: в аннотациях упоминается важное понятие.hot path, то есть вызов метода Do будет частым, а каждый вызов доступа done, done находится в первом поле структуры, доступ к которому возможен напрямую через указатель структуры (для доступа к другим полям требуется вычисление смещения, что медленно)