欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文解析 Golang sync.Once 用法及原理

 更新時(shí)間:2022年08月31日 09:18:40   作者:ag9920???????  
這篇文章主要介紹了一文解析 Golang sync.Once 用法及原理,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下

前言

在此前一篇文章中我們了解了 Golang Mutex 原理解析,今天來看一個(gè)官方給出的 Mutex 應(yīng)用場景:sync.Once。

1. 定位

Once is an object that will perform exactly one action.

sync.Once 是 Go 標(biāo)準(zhǔn)庫提供的使函數(shù)只執(zhí)行一次的實(shí)現(xiàn),常應(yīng)用于單例模式,例如初始化配置、保持?jǐn)?shù)據(jù)庫連接等。它可以在代碼的任意位置初始化和調(diào)用,因此可以延遲到使用時(shí)再執(zhí)行,并發(fā)場景下是線程安全的。

2. 對外接口

Once 對外僅暴露了唯一的方法 Do(f func()),f 為需要執(zhí)行的函數(shù)。

// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
// 	var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// 	config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func()) 

結(jié)合注釋,我們來看看 Do 方法有哪些需要注意的:

  • 只有在當(dāng)前的 Once 實(shí)例第一次調(diào)用 Do 方法時(shí),才會(huì)真正執(zhí)行 f。哪怕在多次調(diào)用 Do 中間 f 的值有所變化,也只會(huì)被實(shí)際調(diào)用一次;
  • Do 針對的是只希望執(zhí)行一次的初始化操作,由于f 是沒有參數(shù)的,如果需要傳參,可以采用包裝一層 func 的形式來實(shí)現(xiàn):config.once.Do(func() { config.init(filename) })
  • 在對f 的調(diào)用返回之前,不會(huì)返回對Do的調(diào)用,所以如果f方法中又調(diào)用來Do方法,將會(huì)死鎖。所以不要做這樣的操作:
func main() {
 var once sync.Once
 once.Do(func() {
    once.Do(func() {
       fmt.Println("hello kenmawr.")
    })
 })
}
  • 如果 f 拋出了 panic,此時(shí)Do會(huì)認(rèn)為f已經(jīng)返回,后續(xù)多次調(diào)用Do也不會(huì)再觸發(fā)對 f 的調(diào)用。

3. 實(shí)戰(zhàn)用法

sync.Once 的場景很多,但萬變不離其宗的落腳點(diǎn)在于:任何只希望執(zhí)行一次的操作。

基于此,我們可以發(fā)現(xiàn)很多具體的應(yīng)用場景落地,比如某個(gè)資源的清理,全局變量的初始化,單例模式等,它們本質(zhì)都是一樣的。這里簡單列幾個(gè),大家可以直接參考代碼熟悉。

3.1 初始化

很多同學(xué)可能會(huì)有疑問,我直接在 init() 函數(shù)里面做初始化不就可以了嗎?效果上是一樣的,為什么還要用 sync.Once,這樣還需要多聲明一個(gè) once 對象。

原因在于:init() 函數(shù)是在所在包首次被加載時(shí)執(zhí)行,若未實(shí)際使用,既浪費(fèi)了內(nèi)存,又延緩了程序啟動(dòng)時(shí)間。而 sync.Once 可以在任何位置調(diào)用,而且是并發(fā)安全的,我們可以在實(shí)際依賴某個(gè)變量時(shí)才去初始化,這樣「延遲初始化」從功能上講并無差異,但可以有效地減少不必要的性能浪費(fèi)。

我們來看 Golang 官方的 html 庫中的一個(gè)例子,我們經(jīng)常使用的轉(zhuǎn)義字符串函數(shù)

func UnescapeString(s string) string

在進(jìn)入函數(shù)的時(shí)候,首先就會(huì)依賴包里內(nèi)置的 populateMapsOnce 實(shí)例(本質(zhì)是一個(gè) sync.Once) 來執(zhí)行初始化 entity 的操作。這里的entity是一個(gè)包含上千鍵值對的 map,如果init()時(shí)就去初始化,會(huì)浪費(fèi)內(nèi)存。

var populateMapsOnce sync.Once
var entity           map[string]rune

func populateMaps() {
    entity = map[string]rune{
        "AElig;":                           '\U000000C6',
        "AMP;":                             '\U00000026',
        "Aacute;":                          '\U000000C1',
        "Abreve;":                          '\U00000102',
        "Acirc;":                           '\U000000C2',
        // 省略后續(xù)鍵值對
    }
}

func UnescapeString(s string) string {
    populateMapsOnce.Do(populateMaps)
    i := strings.IndexByte(s, '&')

    if i < 0 {
            return s
    }
    // 省略后續(xù)的實(shí)現(xiàn)
    ...
}

3.2 單例模式

開發(fā)中我們經(jīng)常會(huì)實(shí)現(xiàn) Getter 來暴露某個(gè)非導(dǎo)出的變量,這個(gè)時(shí)候就可以把 once.Do 放到 Getter 里面,完成單例的創(chuàng)建。

package main
import (
   "fmt"
   "sync"
)
type Singleton struct{}
var singleton *Singleton
var once sync.Once

func GetSingletonObj() *Singleton {
   once.Do(func() {
      fmt.Println("Create Obj")
      singleton = new(Singleton)
   })
   return singleton
}
func main() {
   var wg sync.WaitGroup
   for i := 0; i < 5; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         obj := GetSingletonObj()
         fmt.Printf("%p\n", obj)
      }()
   }
   wg.Wait()
}
/*--------- 輸出 -----------
Create Obj
0x119f428
0x119f428
0x119f428
0x119f428
0x119f428
**/

3.3 關(guān)閉channel

一個(gè)channel如果已經(jīng)被關(guān)閉,再去關(guān)閉的話會(huì) panic,此時(shí)就可以應(yīng)用 sync.Once 來幫忙。

type T int
type MyChannel struct {
   c    chan T
   once sync.Once
}
func (m *MyChannel) SafeClose() {
   // 保證只關(guān)閉一次channel
   m.once.Do(func() {
      close(m.c)
   })
}

4. 原理

在 sync 的源碼包中,Once 的定義是一個(gè) struct,所有定義和實(shí)現(xiàn)去掉注釋后不過 30行,我們直接上源碼來分析:

package sync
import (
   "sync/atomic"
)
// 一個(gè) Once 實(shí)例在使用之后不能被拷貝繼續(xù)使用
type Once struct {
   done uint32 // done 表明了動(dòng)作是否已經(jīng)執(zhí)行
   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()
   }
}

這里有兩個(gè)非常巧妙的設(shè)計(jì)值得學(xué)習(xí),我們參照注釋來看一下:

  • 結(jié)構(gòu)體字段順序?qū)λ俣鹊挠绊?我們來看一下帶注釋的 Once 結(jié)構(gòu)定義
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/386),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}

sync.Once絕大多數(shù)場景都會(huì)訪問o.done,訪問 done 的機(jī)器指令是處于hot path上,hot path表示程序非常頻繁執(zhí)行的一系列指令。由于結(jié)構(gòu)體第一個(gè)字段的地址和結(jié)構(gòu)體的指針是相同的,如果是第一個(gè)字段,直接對結(jié)構(gòu)體的指針解引用即可,如果是其他的字段,除了結(jié)構(gòu)體指針外,還需要計(jì)算與第一個(gè)值的偏移,所以將done放在第一個(gè)字段,則CPU減少了一次偏移量的計(jì)算,訪問速度更快。

  • 為何不使用 CAS 來達(dá)到執(zhí)行一次的效果

其實(shí)使用 atomic.CompareAndSwapUint32 是一個(gè)非常直觀的方案,這樣的話 Do 的實(shí)現(xiàn)就變成了

func (o *OnceA) Do(f func()) {
  if !atomic.CompareAndSwapUint32(&o.done, 0, 1) {
    return
  }
  f()
}

這樣的問題在于,一旦出現(xiàn) CAS 失敗的情況,成功協(xié)程會(huì)繼續(xù)執(zhí)行 f,但其他失敗協(xié)程不會(huì)等待 f 執(zhí)行結(jié)束。而Do 的API定位對此有著強(qiáng)要求,當(dāng)一次 once.Do 返回時(shí),執(zhí)行的 f 一定是完成的狀態(tài)。

對此,sync.Once 官方給出的解法是:

Slow path falls back to a mutex, and the atomic.StoreUint32 must be delayed until after f returns.

我們再來結(jié)合 doSlow() 看一看這里是怎么解決這個(gè)并發(fā)問題的:

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()
   }
}
  • atomic.LoadUint32 用于原子加載地址(也就是 &o.done),返回加載到的值;
  • o.done 為 0 是代表尚未執(zhí)行。若同時(shí)有兩個(gè) goroutine 進(jìn)來,發(fā)現(xiàn) o.done 為 0(此時(shí) f 尚未執(zhí)行),就會(huì)進(jìn)入 o.doSlow(f) 的慢路徑中(slow path);
  • doSlow 使用 sync.Mutex 來加鎖,一個(gè)協(xié)程進(jìn)去,其他的被阻塞在鎖的地方(注意,此時(shí)是阻塞,不是直接返回,這是和 CAS 方案最大的差別);
  • 經(jīng)過 o.m.Lock() 獲取到鎖以后,如果此時(shí) o.done 還是 0,意味著依然沒有被執(zhí)行,此時(shí)就可以放心的調(diào)用 f來執(zhí)行了。否則,說明當(dāng)前協(xié)程在被阻塞的過程中,已經(jīng)失去了調(diào)用f 的機(jī)會(huì),直接返回。
  • defer atomic.StoreUint32(&o.done, 1) 是這里的精華,必須等到f() 返回,在 defer 里才能夠去更新 o.done 的值為 1。

5. 避坑

  • 不要拷貝一個(gè) sync.Once 使用或作為參數(shù)傳遞,然后去執(zhí)行 Do,值傳遞時(shí) done 會(huì)歸0,無法起到限制一次的效果。
  • 不要在 Do 的 f 中嵌套調(diào)用 Do。

到此這篇關(guān)于一文解析 Golang sync.Once 用法及原理的文章就介紹到這了,更多相關(guān) Golang sync.Once 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Golang如何編寫內(nèi)存高效及CPU調(diào)優(yōu)的Go結(jié)構(gòu)體

    Golang如何編寫內(nèi)存高效及CPU調(diào)優(yōu)的Go結(jié)構(gòu)體

    這篇文章主要介紹了Golang如何編寫內(nèi)存高效及CPU調(diào)優(yōu)的Go結(jié)構(gòu)體,結(jié)構(gòu)體是包含多個(gè)字段的集合類型,用于將數(shù)據(jù)組合為記錄
    2022-07-07
  • Golang比較兩個(gè)slice是否相等的問題

    Golang比較兩個(gè)slice是否相等的問題

    本文主要介紹了Golang比較兩個(gè)slice是否相等的問題,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-03-03
  • Go Gin實(shí)現(xiàn)文件上傳下載的示例代碼

    Go Gin實(shí)現(xiàn)文件上傳下載的示例代碼

    這篇文章主要介紹了Go Gin實(shí)現(xiàn)文件上傳下載的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-04-04
  • golang json數(shù)組拼接的實(shí)例

    golang json數(shù)組拼接的實(shí)例

    這篇文章主要介紹了golang json數(shù)組拼接的實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-04-04
  • 詳解Go語言中for range的

    詳解Go語言中for range的"坑"

    這篇文章主要介紹了詳解Go語言中for range的"坑",文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-09-09
  • golang通過gorm操作sqlite設(shè)置主鍵自增的步驟

    golang通過gorm操作sqlite設(shè)置主鍵自增的步驟

    這篇文章主要介紹了golang通過gorm操作sqlite設(shè)置主鍵自增的詳細(xì)步驟,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2023-09-09
  • GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法

    GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法

    這篇文章主要介紹了GOLANG使用Context實(shí)現(xiàn)傳值、超時(shí)和取消的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2019-01-01
  • 詳解Go語言的內(nèi)存模型及堆的分配管理

    詳解Go語言的內(nèi)存模型及堆的分配管理

    這篇筆記主要介紹Go內(nèi)存分配和Go內(nèi)存管理,會(huì)輕微涉及內(nèi)存申請和釋放,以及Go垃圾回收,文中有詳細(xì)的代碼示例以及圖片介紹,需要的朋友可以參考下
    2023-05-05
  • Golang常用包使用介紹

    Golang常用包使用介紹

    標(biāo)準(zhǔn)的Go語言代碼庫中包含了大量的包,并且在安裝Go的時(shí)候多數(shù)會(huì)自動(dòng)安裝到系統(tǒng)中。我們可以在$GOROOT/src/pkg目錄中查看這些包。下面簡單介紹一些我們開發(fā)中常用的包
    2022-09-09
  • Go語言中函數(shù)可變參數(shù)(Variadic Parameter)詳解

    Go語言中函數(shù)可變參數(shù)(Variadic Parameter)詳解

    在Python中,在函數(shù)參數(shù)不確定數(shù)量的情況下,可以動(dòng)態(tài)在函數(shù)內(nèi)獲取參數(shù)。在Go語言中,也有類似的實(shí)現(xiàn)方式,本文就來為大家詳細(xì)講解一下
    2022-07-07

最新評論