1. 程式人生 > >Go基礎之鎖的初識

Go基礎之鎖的初識

當我們的程式就一個執行緒的時候是不需要用到鎖的,但是通常我們實際的程式碼不會是單個執行緒的,所有這個時候就需要用到鎖了,那麼關於鎖的使用場景主要涉及到哪些呢?

  • 當我們多個執行緒在讀相同的資料的時候則是需要加鎖的
  • 當我們的程式既有讀又有寫的時候更是需要加鎖的
  • 當我們有多個執行緒在寫的時候同樣也是需要加鎖

互斥鎖

互斥鎖:同一個時刻只有一個執行緒能夠拿到鎖

我們先通過一個例子來演示,如果當多個執行緒同時更改一個變數,結果會是怎麼樣
不加鎖版本

package main

import (
    "sync"
    "fmt"
)

var (
    //lock sync.Mutex
    count int
    w sync.WaitGroup  //用於等待子執行緒執行完之後退出
)

func main() {
    w.Add(1) // 在呼叫執行緒前執行w.add
    go func(){
        for i:=0;i<100000;i++{
            count++
        }
        w.Done()  //執行完 執行w.Done
    }()
    for i :=0;i<100000;i++{
        count++
    }
    w.Wait() // 最後執行w.wait等待所有的執行緒執行完畢
    fmt.Println(count)

}

當我們執行多次就可以發現,最後的結果基本不可能是我們先看到的:200000
我們修改程式碼程式碼需要加鎖保護的地方加上鎖,並且這裡加的是互斥鎖,修改後的程式碼為:

package main

import (
    "sync"
    "fmt"
)

var (
    lock sync.Mutex
    count int
    w sync.WaitGroup  //用於等待子執行緒執行完之後退出
)

func main() {
    w.Add(1) // 在呼叫執行緒前執行w.add
    go func(){
        for i:=0;i<100000;i++{
            lock.Lock()
            count++
            lock.Unlock()
        }
        w.Done()  //執行完 執行w.Done
    }()
    for i :=0;i<100000;i++{
        lock.Lock()
        count++
        lock.Unlock()
    }
    w.Wait() // 最後執行w.wait等待所有的執行緒執行完畢
    fmt.Println(count)

}

這次當我們多次執行的時候,就能保證我們每次都能看到我們想要的值:200000
接下來看讀寫鎖

讀寫鎖

讀寫鎖主要用到讀多寫少的場景
讀寫鎖分為:讀鎖和寫鎖

如果自己設定了一個寫鎖,那麼其他讀的執行緒以及寫的執行緒都拿不到鎖,這個時候和互斥鎖的功能相同
如果自己設定了一個讀鎖,那麼其他寫的執行緒是拿不到鎖的,但是其他讀的執行緒都是可以拿到這個鎖

我們把上面的例子程式碼進行更改:

package main

import (
    "sync"
    "fmt"
)    

var (
    rwlock sync.RWMutex
    w sync.WaitGroup
    count int
)


func main() {
    w.Add(1)
    go func(){
        for i:=0;i<1000000;i++{
            rwlock.Lock() // 這裡定義了一個寫鎖
            count++
            rwlock.Unlock()
        }
        w.Done()
    }()

    for i:=0;i<1000000;i++{
        rwlock.Lock() // 這裡定義了一個寫鎖
        count++
        rwlock.Unlock()
    }
    w.Wait()
    fmt.Println(count)
}

通過設定寫鎖,我們同樣可以實現資料的一致性
下面是一個讀鎖的使用例子:

package main

import (
    "sync"
    "fmt"
)

var (
    rwlock sync.RWMutex
    w sync.WaitGroup
    count int
)


func main() {
    w.Add(1)
    go func(){
        for i:=0;i<1000000;i++{
            rwlock.Lock() // 這裡定義了一個寫鎖
            count++
            rwlock.Unlock()
        }
        w.Done()
    }()

    for i:=0;i<16;i++{
        w.Add(1)
        go func(){
            rwlock.RLock() //這裡定義了一個讀鎖
            fmt.Println(count)
            rwlock.RUnlock() //釋放讀鎖
            w.Done()
        }()
    }
    w.Wait()
    fmt.Println(count)
}

Go中的原子操作

原子操作,我們則不需加鎖,也能保證資料的一致性
並且如果只是計算,那麼原子操作則是最快的

例項程式碼:

package main

import (
    "sync"
    //"time"
    "sync/atomic"
    "fmt"
)

var (
    w sync.WaitGroup
    count int32
)


func main() {
    w.Add(1)
    //start := time.Now().UnixNano()
    go func() {
        for i:=0;i<1000000;i++{
            atomic.AddInt32(&count,1)
        }
        w.Done()
    }()

    for i:=0;i<1000000;i++{
        atomic.AddInt32(&count,1)
    }
    w.Wait()
    //end := time.Now().UnixNano()
    //fmt.Println((end- start)/1000/1000)
    fmt.Println(count)
}

關於互斥鎖的補充

互斥鎖需要注意的問題:

1、不要重複鎖定互斥鎖

2、不要忘記解鎖互斥鎖, 必要時使用defer語句

3、不要對尚未鎖定或者已解鎖的互斥鎖解鎖

4、不要對在多個函式之間直接傳遞互斥鎖

對已經鎖定的互斥鎖進行鎖定,會立即阻塞當前的goroutine 這個goroutine所執行的流程會一直停滯在該呼叫互斥鎖的Lock方法的那行程式碼

所謂死鎖: 當前程式中的主goroutine以及我們啟用的那些goroutine 都已經被阻塞,這些goroutine可以被稱為使用者級的goroutine 這就相當於整個程式已經停滯不前了,並且這個時候go程式會丟擲如下的panic:

fatal error: all goroutines are asleep - deadlock!

並且go語言執行時系統丟擲自行丟擲的panic都屬於致命性錯誤,都是無法被恢復的,呼叫recover函式對他們起不到任何作用

Go語言中的互斥鎖是開箱即用的,也就是我們宣告一個sync.Mutex 型別的變數,就可以直接使用它了,需要注意:該型別是一個結構體型別,屬於值型別的一種,把它傳給一個函式將它從函式中返回,把它賦值給其他變數,讓它進入某個通道都會導致他的副本的產生。並且原值和副本以及多個副本之間是完全獨立的,他們都是不同的互斥鎖所以不應該將鎖通過函式的引數進行傳遞

關於讀寫鎖的補充

1、在寫鎖已被鎖定的情況下再次試圖鎖定寫鎖,會阻塞當前的goroutine

2、在寫鎖已被鎖定的情況下再次試圖鎖定讀鎖,也會阻塞當前的goroutine

3、在讀鎖已被鎖定的情況下試圖鎖定寫鎖,同樣會阻塞當前的goroutine

4、在讀鎖已被鎖定的情況下再試圖鎖定讀鎖,並不會阻塞當前的goroutine

對於某個受到讀寫鎖保護的共享資源,多個寫操作不能同時進行,寫操作和讀操作也不能同時進行,但多個讀操作卻可以同時進行

對寫鎖進行解鎖,會喚醒“所有因試圖鎖定讀鎖,而被阻塞的goroutine”, 並且這個通常會使他們都成功完成對讀鎖的鎖定

對讀鎖進行解鎖,只會在沒有其他讀鎖鎖定的前提下,喚醒“因試圖鎖定寫鎖,而被阻塞的goroutine” 並且只會有一個被喚醒的goroutine能夠成功完成對寫鎖的鎖定,其他的goroutine

還要在原處繼續等待,至於哪一個goroutine,那麼就要看誰等待的事件最長

解鎖讀寫鎖中未被鎖定的寫鎖, 會立即引發panic ,對其中的讀鎖也是如此,並且同樣是不可恢復的