Golang同步相關知識總結

1.互斥鎖
表示:sync.Mutex,類型sync.Mutex的零值表示了未被鎖定的互斥量
作用:保證在同一時刻僅有一個線程訪問共享數據。
規則:1)當對一個已處於解鎖狀態的互斥鎖進行解鎖操作的時候,就會引發一個運行時恐慌;2)當對一個已處於鎖定狀態的互斥鎖進行鎖定操作時,就會被阻塞;3)對於同一個互斥鎖的鎖定操作和解鎖操作總是應該成對地出現,一般會在鎖定互斥鎖之後緊接着用defer語句保證該互斥鎖的及時解鎖;4)互斥鎖可以被直接的在多個Goroutine之間共享但是還是建議把對同個互斥鎖的鎖定和解鎖操作放在同一個層次的代碼塊中,應該使代表互斥鎖的變量的訪問權限盡量低,避免在不相關的流程中被誤用,導致程序不正確的行爲。
方法:Lock和Unlock,表示對互斥量的鎖定和解鎖。
示例:

func Lock1(){
    var lock sync.Mutex
    fmt.Println("main>lock is locking")
    lock.Lock()
    fmt.Println("main>lock is locked")
    go func(){
        for i:=0;i<3;i++{
            fmt.Println("go routine>lock is locking",i)
            lock.Lock()
            fmt.Println("go routine>lock is locked",i)
        }
    }()
    time.Sleep(time.Second)
    fmt.Println("main>lock is unlocking")
    lock.Unlock()
    fmt.Println("main>lock is unlocked")
    time.Sleep(5*time.Second)
}

運行結果:
=== RUN TestLock1
main>lock is locking
main>lock is locked
go routine>lock is locking 0
main>lock is unlocking
main>lock is unlocked
go routine>lock is locked 0
go routine>lock is locking 1
--- PASS: TestLock1 (6.00s)
PASS

2.讀寫鎖
表示:sync.RWMutex,其零值已經是立即可用的讀寫鎖了,可分別針對讀操作和寫操作進行鎖定和解鎖操作。
規則:1)寫解鎖在進行的時候會試圖喚醒所有因欲進行讀鎖定而被阻塞的Goroutine,讀解鎖在進行的時候會在已無任何讀鎖定的情況下試圖喚醒一個因欲進行寫鎖定而被阻塞的Goroutine。2)對一個未被寫鎖定的的讀寫鎖進行寫解鎖,會引發恐慌;而對一個未被讀鎖定的讀寫鎖進行讀解鎖則不會。3)對於同一個讀寫鎖來說,施加在它之上的讀鎖定可以有多個。簡單說,讀寫鎖控制下的多個寫操作之間是互斥的;寫操作和讀操作是互斥的;多個讀操作之間不存在互斥關係。
方法:Lock,Unlock,RLock,RUnlock,RLocker,分別表示寫鎖定,寫解鎖,讀鎖定,讀解鎖,最後一個方法會返回一個實現了sync.Locker接口的值,這個結果的Lock和Unlock方法分別針對該讀寫鎖的讀鎖定和讀解鎖操作。其意義在於可以在以後以相同的方式對該讀寫鎖中的寫鎖和讀鎖進行操作。
示例:
對同一個文件進行操作:寫操作之間不能彼此干擾、讀操作按順序獨立的操作。

func (df *myDataFile) Read() (d Data, err error) {
    // 讀取並更新讀偏移量offset
    ...

    //讀取一個數據塊,fmutex爲讀寫鎖
    bytes := make([]byte, df.dataLen)
    for {
        df.fmutex.RLock()  //讀鎖定
        _, err = df.f.ReadAt(bytes, offset)
        if err != nil {
            if err == io.EOF {
                df.fmutex.RUnlock() //讀解鎖
                continue
            }
            df.fmutex.RUnlock() //讀解鎖
            return
        }
        d = bytes
        df.fmutex.RUnlock() //讀解鎖
        return
    }
}

func (df *myDataFile) Write(d Data) (err error) {
    ...

    //寫入一個數據塊
    df.fmutex.Lock() //寫鎖定
    defer df.fmutex.Unlock() //寫解鎖
    _, err = df.f.Write(d)
    return
}

條件變量

表示:sync.Cond,區別於互斥鎖,簡單的聲明無法創建出一個可用的條件變量,需要使用sync.NewCond。條件變量需要與互斥量結合使用
作用:共享數據的狀態發生變化時,通知其他因此而被阻塞的線程。
方法:Wait、Signal、Broadcast,分別表示等待通知、單發通知、廣播通知。後兩者作用是發送通知以喚醒正在爲此被阻塞的Goroutine。前者會自動地對與該條件變量關聯的那個鎖進行解鎖,並使調用方所在的Goroutine被阻塞。
Wait:阻塞當前線程,直至收到該條件變量發來的通知。
Signal:讓該條件變量向至少一個正在等待它的通知的線程發送通知,以表示某個共享數據的狀態已經改變。
Broadcast:讓條件變量給正在等待它的通知的所有線程都發送通知,以表示某個共享數據的狀態已經改變。
signal和broadcast方法調用之前無需鎖定與之關聯的鎖,而wait需要。
示例:

df.rcond = sync.NewCond(df.fmutex.RLocker())
func (df *myDataFile) Read() (d Data, err error) {
    // 讀取並更新讀偏移量
    ...

    //讀取一個數據塊,fmutex爲讀寫鎖,rcond爲讀操作需要用到的條件變量
    bytes := make([]byte, df.dataLen)
    df.fmutex.RLock() //讀鎖定
    defer df.fmutex.RUnlock() //讀解鎖
    for {
        _, err = df.f.ReadAt(bytes, offset)
        if err != nil {
            if err == io.EOF {
                df.rcond.Wait() //條件變量等待通知
                continue
            }
            return
        }
        d = bytes
        return
    }
}

func (df *myDataFile) Write(d Data) (err error) {
    ...

    //寫入一個數據塊
    df.fmutex.Lock() //寫鎖定
    defer df.fmutex.Unlock() //寫解鎖
    _, err = df.f.Write(d) 
    df.rcond.Signal() //條件變量單發通知
    return
}

原子操作

相關包:sync/atomic,原子操作即進行過程中不能被中斷的操作。
類型包括:int32,int64,uint32,uint64,uintptr,unsafe.Pointer
操作:增或減add、比較並交換compare and swap,CAS、載入load、存儲store、交換swap
增或減
atomic.AddInt32(&a,3)
atomic.AddInt64(&a,-3),需注意uint32,uint64減不是這麼運算的,其運算如下:
atomic.AddUint32(&b,^uint32(-NN-1)),其中NN表示一個負整數。
比較並交換
CompareAndSwapInt32(addr &int32,old,new int32)(swapped bool)
併發安全地更新一些類型的值,優先選擇CAS(比較並交換)。比較並交換操作即 CAS 操作,是有條件的交換操作,只有在條件滿足的情況下才會進行值的交換。所謂的交換指的是,把新值賦給變量,並返回變量的舊值。在進行 CAS 操作的時候,函數會先判斷被操作變量的當前值,是否與我們預期的舊值相等。如果相等,它就把新值賦給該變量,並返回true以表明交換操作已進行;否則就忽略交換操作,並返回false。可以看到,CAS 操作並不是單一的操作,而是一種操作組合。這與其他的原子操作都不同。正因爲如此,它的用途要更廣泛一些。例如,我們將它與for語句聯用就可以實現一種簡易的自旋鎖(spinlock)。
載入
v:=atomic.LoadInt32(&value)原子地讀取變量value的值,當前計算機中的任何CPU都不會進行其他針對此值的讀或寫操作
存儲
StoreInt32
原子地存儲某個值的過程中,任何CPU都不會進行鍼對同一個值的讀或寫操作。
交換
SwapInt32
直接設置新值,返回舊值

一旦我們確定了在某個場景下可以使用原子操作函數,比如:只涉及併發地讀寫單一的整數類型值,或者多個互不相關的整數類型值,那就不要再考慮互斥鎖了。原子操作一定程度可以替換鎖。原子操作由底層硬件支持,鎖由操作系統提供的API實現。
示例:

func (df *myDataFile) Read() (rsn int64, d Data, err error) {
    // 讀取並更新讀偏移量,roffset表示讀操作需要用到的偏移量
    var offset int64
    for {
        offset = atomic.LoadInt64(&df.roffset) //原子地讀取roffset
        //這裏進行了CAS操作,依次傳入三個值:被操作值的地址、被操作數的舊值、欲設置的新值
        if atomic.CompareAndSwapInt64(&df.roffset, offset, (offset + int64(df.dataLen))) {
            break
        }
    }

    //讀取一個數據塊
    ...
}

只會執行一次

表示:sync.Once
特點:once.Do:無論我們在多次調用時傳遞給它的參數是否相同,都僅有第一次調用是有效的。
應用場景:執行僅需執行一次的任務。比如數據庫連接池的初始化任務;一些需持續運動的實時監測任務。
連接數據庫的代碼其實不太適合放到 Do 裏面執行,或者說不太恰當。初始化數據庫鏈接的代碼可以放到裏面。而斷鏈重連的機制也應該在其中。
方法:Once類型的Do方法只接受一個參數,這個參數的類型必須是func(),即:無參數聲明和結果聲明的函數。Once類型中還有一個名叫done的uint32類型的字段。它的作用是記錄其所屬值的Do方法被調用的次數。不過,該字段的值只可能是0或者1。一旦Do方法的首次調用完成,它的值就會從0變爲1。
示例:

func OnceDo(){
    var num int
    sign:=make(chan bool)
    var once sync.Once
    f:=func(ii int)func(){
        return func(){
            num=(num+ii*2)
            sign<-true
        }
    }

    for i:=0;i<3;i++{
        fi:=f(i+1)
        go once.Do(fi)
    }

    for j:=0;j<3;j++{
        select{
        case <-sign:
            fmt.Println("received a signal!")
        case <-time.After(time.Second):
            fmt.Println("time out!")
        }
    }
    fmt.Println("num=",num)
}

運行結果:
received a signal!
time out!
time out!
num= 2

WaitGroup
表示:sync.WaitGroup,併發安全的,可以對多個Goroutine的運行進行簡單的協調。它比通道更加適合實現這種一對多的 goroutine 協作流程。
規則:Add方法的調用應該在Done方法之前或Wait方法之前。
方法:Add,Done,Wait。分別表示增大或減少其中的計數值、計數值減一、檢查該值中的計數值。其中Wait方法會判斷如果計數值爲0,則立即返回,不會對程序的運行產生任何影響;如果計數值>0,那麼該方法的調用方所屬的那個Goroutine就會被阻塞,直到該計數值重新變爲0,爲此而阻塞的所有GoRoutine纔會被喚醒。
示例:

func WaitGroupOp(){
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        fmt.Println("go routine")
        wg.Done()
    }()
    wg.Wait()
}

context.Context

定義:上下文,或者說goroutine的上下文。Context是線程安全的,可以放心的在多個goroutine中傳遞。
場景:不能在一開始就確定執行子任務的 goroutine 的數量;有很多goroutine都需要控制結束;goroutine又衍生了其他更多的goroutine、一層層的無窮盡的goroutine等goroutine較複雜的關係鏈。
方法:Deadline、Done、Err、Value。
Deadline:獲取設置的截止時間的意思,第一個返回式是截止時間,到了這個時間點,Context會自動發起取消請求;第二個返回值ok==false時表示沒有設置截止時間,如果需要取消的話,需要調用取消函數進行取消。
Done:返回一個只讀的chan,類型爲struct{},我們在goroutine中,如果該方法返回的chan可以讀取,則意味着parent context已經發起了取消請求,我們通過Done方法收到這個信號後,就應該做清理操作,然後退出goroutine,釋放資源。這個接收通道的用途並不是傳遞元素值,而是讓調用方去感知“撤銷”當前Context值的那個信號。
Err:返回取消的錯誤原因,因爲什麼Context被取消。
Value:獲取該Context上綁定的值,是一個鍵值對,所以要通過一個Key纔可以獲取對應的值,這個值一般是線程安全的。在我們調用含數據的Context值的Value方法時,它會先判斷給定的鍵,是否與當前值中存儲的鍵相等,如果相等就把該值中存儲的值直接返回,否則就到其父值中繼續查找。如果其父值中仍然未存儲相等的鍵,那麼該方法就會沿着上下文根節點的方向一路查找下去。注意,Context接口並沒有提供改變數據的方法。因此,在通常情況下,我們只能通過在上下文樹中添加含數據的Context值來存儲新的數據,或者通過撤銷此種值的父值丟棄掉相應的數據。如果你存儲在這裏的數據可以從外部改變,那麼必須自行保證安全。
理解:所有的Context值共同構成了一顆代表了上下文全貌的樹形結構。這棵樹的樹根(或者稱上下文根節點)是一個已經在context包中預定義好的Context值,它是全局唯一的。通過調用context.Background函數,我們就可以獲取到它。這裏注意一下,這個上下文根節點僅僅是一個最基本的支點,它不提供任何額外的功能。也就是說,它既不可以被撤銷(cancel),也不能攜帶任何數據。但是context包中包含了四個用於繁衍Context值的函數,即:WithCancel、WithDeadline、WithTimeout和WithValue。這些函數的第一個參數的類型都是context.Context,而名稱都爲parent,表示將會產生的Context值的父值。
示例:

/*
context.Background() 返回一個空的Context,這個空的Context一般用於整個Context樹的根節點。然後我們使用context.WithCancel(parent)函數,創建一個可取消的子Context,然後當作參數傳給goroutine使用,這樣就可以使用這個子Context跟蹤這個goroutine。
示例中啓動了3個監控goroutine進行不斷的監控,每一個都使用了Context進行跟蹤,當我們使用cancel函數通知取消時,這3個goroutine都會被結束。這就是Context的控制能力,它就像一個控制器一樣,按下開關後,所有基於這個Context或者衍生的子Context都會收到通知,這時就可以進行清理操作了,最終釋放goroutine,這就優雅的解決了goroutine啓動後不可控的問題。
 */
func ContextOp() {
    ctx, cancel := context.WithCancel(context.Background())
    go watch(ctx,"【監控1】")
    go watch(ctx,"【監控2】")
    go watch(ctx,"【監控3】")

    time.Sleep(1 * time.Second)
    fmt.Println("可以了,通知監控停止")
    cancel()
    //爲了檢測監控過是否停止,如果沒有監控輸出,就表示停止了
    time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name,ctx.Err(),"監控退出,停止了...")
            return
        default:
            fmt.Println(name,"goroutine監控中...")
            time.Sleep(2 * time.Second)
        }
    }
}
func ContextOp1(){
    ctx,cancel:=context.WithCancel(context.Background())
    go watch1(context.WithValue(ctx,"key","監控1"))
    go watch1(context.WithValue(ctx,"key","監控2"))
    go watch1(context.WithValue(ctx,"key","監控3"))

    time.Sleep(1 * time.Second)
    fmt.Println("可以了,通知監控停止")
    cancel()
    time.Sleep(5 * time.Second)
}
func watch1(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(ctx.Value("key"),ctx.Err(),"監控退出,停止了...")
            return
        default:
            fmt.Println(ctx.Value("key"),"goroutine監控中...")
            time.Sleep(2 * time.Second)
        }
    }
}

臨時對象池

表示:sync.Pool,非開箱即用。sync.Pool的New字段代表着創建臨時對象的函數,其類型是沒有參數但有唯一結果的函數類型,即:func() interface{}。存放可被重複使用的值的容器,此類容器是自動伸縮的,高效的,同時也是併發安全的,它們的創建和銷燬可以在任何時候發生,並且完全不影響程序的性能。
方法:Get和Put。從當前的池中獲取臨時對象,返回一個interface{}類型的值;在當前的池中存放臨時對象,接受一個interface{}類型的值。
這個類型的Get方法可能會從當前的池中刪除掉任何一個值,然後把這個值作爲結果返回。如果此時當前的池中沒有任何值,那麼這個方法就會使用當前池的New字段創建一個新值,並直接將其返回。
特性:臨時對象池可以把由其中的對象值產生的存儲壓力進行分攤,它會專門爲每一個與操作它的Goroutine相關聯的P都生成一個本地池;對垃圾回收友好,垃圾回收的執行一般會使臨時對象池中的對象值被全部移除。
場景:不需要持久使用的某一類值,無需被區分,其中的任何一個值都可以代替另一個,因此可以將臨時對象池當作針對某種臨時且狀態無關的數據的緩存來用
示例:
func main() {
// 禁用GC,並保證在main函數執行結束前恢復GC
defer debug.SetGCPercent(debug.SetGCPercent(-1))
var count int32
newFunc := func() interface{} {
return atomic.AddInt32(&count, 1)
}
pool := sync.Pool{New: newFunc}

// New 字段值的作用
v1 := pool.Get()
fmt.Printf("v1: %v\n", v1)

// 臨時對象池的存取
pool.Put(newFunc())
pool.Put(newFunc())
pool.Put(newFunc())
v2 := pool.Get()
fmt.Printf("v2: %v\n", v2)

// 垃圾回收對臨時對象池的影響
debug.SetGCPercent(100)
runtime.GC()
v3 := pool.Get()
fmt.Printf("v3: %v\n", v3)
pool.New = nil
v4 := pool.Get()
fmt.Printf("v4: %v\n", v4)

}
運行結果:
v1: 1
v2: 2
v3: 5
v4: <nil>

參考資料
《Go併發編程實戰》
Go語言核心36講

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章