sync.Once和自己加鎖有什麼區別嗎?

so_easy發表於2022-03-18

單例

func loadIcons() {
    icons = map[string]string{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
}

// Icon 被多個goroutine呼叫時不是併發安全的
//每個goroutine都滿足序列一致的基礎上自由地重排訪問記憶體的順序
func Icon(name string) string {
    if icons == nil {
      loadIcons()
    }
    return icons[name]
}

這個和JAVA中的單例寫法基本一致。但是當初寫JAVA單例時,會加上關鍵字 synchronize 。但是加上這個還無法保證單例一定就會只執行一次並且按預想的執行。因為計算機執行指令時滿足序列一致的基礎上自由地重排訪問記憶體的順序。解釋就是:指令的執行可能在沒有上下文依賴的情況下,可以先執行其他的指令。這就可能導致多協程下判斷 icons == nil 時 icons 還沒全部初始化完成,卻走到 icons[name],此時無法返回預期結果。於是JAVA的做法就是防止指令重排順序,JVM利用的是計算機內部的讀寫柵欄隔離,其實類似加鎖。關鍵字是:[volatile](https://blog.csdn.net/xiaolyuh123/article/details/103289570 "volatile")

同理,Go的單例最佳化後也可以加鎖:

var lock sync.Mutex
func Icon(name string) string {
    lock.Lock() //加鎖
    if icons == nil {
      loadIcons()
    }
    lock.Unlock()
    return icons[name]
}

但是Go在底層已經實現了只執行一次的方法,如下:

var loadIconsOnce sync.Once
func Icon(name string) string {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

思考:sync.Once 執行一次和自己加鎖的有什麼區別嗎?

歡迎你的回答,等你的回答!!!


最近藉著大家的評論又看了下Go的原始碼,之前沒有注意到Go的原子操作,於是去看了下。現在再回來看就很清楚了。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章