《Go語言四十二章經(jīng)》第二十三章 同步與鎖

《Go語言四十二章經(jīng)》第二十三章 同步與鎖

作者:李驍

23.1 同步鎖

Go語言包中的sync包提供了兩種鎖類型:sync.Mutex和sync.RWMutex懊昨,前者是互斥鎖室抽,后者是讀寫鎖架谎。

互斥鎖是傳統(tǒng)的并發(fā)程序?qū)蚕碣Y源進行訪問控制的主要手段,在Go中姓赤,似乎更推崇由channel來實現(xiàn)資源共享和通信椅寺。它由標(biāo)準(zhǔn)庫代碼包sync中的Mutex結(jié)構(gòu)體類型代表。只有兩個公開方法:調(diào)用Lock()獲得鎖巫员,調(diào)用unlock()釋放鎖。

  • 使用Lock()加鎖后甲棍,不能再繼續(xù)對其加鎖(同一個goroutine中简识,即:同步調(diào)用),否則會panic感猛。只有在unlock()之后才能再次Lock()财异。異步調(diào)用Lock(),是正當(dāng)?shù)逆i競爭唱遭,當(dāng)然不會有panic了。適用于讀寫不確定場景呈驶,即讀寫次數(shù)沒有明顯的區(qū)別拷泽,并且只允許只有一個讀或者寫的場景,所以該鎖也叫做全局鎖袖瞻。

  • func (m *Mutex) Unlock()用于解鎖m司致,如果在使用Unlock()前未加鎖,就會引起一個運行錯誤聋迎。已經(jīng)鎖定的Mutex并不與特定的goroutine相關(guān)聯(lián)脂矫,這樣可以利用一個goroutine對其加鎖,再利用其他goroutine對其解鎖霉晕。

建議:同一個互斥鎖的成對鎖定和解鎖操作放在同一層次的代碼塊中庭再。
使用鎖的經(jīng)典模式:

var lck sync.Mutex
func foo() {
    lck.Lock() 
    defer lck.Unlock()
    // ...
}

lck.Lock()會阻塞直到獲取鎖捞奕,然后利用defer語句在函數(shù)返回時自動釋放鎖。

下面代碼通過3個goroutine來體現(xiàn)sync.Mutex 對資源的訪問控制特征:

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    wg := sync.WaitGroup{}

    var mutex sync.Mutex
    fmt.Println("Locking  (G0)")
    mutex.Lock()
    fmt.Println("locked (G0)")
    wg.Add(3)

    for i := 1; i < 4; i++ {
        go func(i int) {
            fmt.Printf("Locking (G%d)\n", i)
            mutex.Lock()
            fmt.Printf("locked (G%d)\n", i)

            time.Sleep(time.Second * 2)
            mutex.Unlock()
            fmt.Printf("unlocked (G%d)\n", i)
            wg.Done()
        }(i)
    }

    time.Sleep(time.Second * 5)
    fmt.Println("ready unlock (G0)")
    mutex.Unlock()
    fmt.Println("unlocked (G0)")

    wg.Wait()
}
程序輸出:
Locking  (G0)
locked (G0)
Locking (G1)
Locking (G3)
Locking (G2)
ready unlock (G0)
unlocked (G0)
locked (G1)
unlocked (G1)
locked (G3)
locked (G2)
unlocked (G3)
unlocked (G2)

通過程序執(zhí)行結(jié)果我們可以看到拄轻,當(dāng)有鎖釋放時颅围,才能進行l(wèi)ock動作,GO鎖釋放時恨搓,才有后續(xù)鎖釋放的可能院促,這里是G1搶到釋放機會。

Mutex也可以作為struct的一部分斧抱,這樣這個struct就會防止被多線程更改數(shù)據(jù)常拓。

package main

import (
    "fmt"
    "sync"
    "time"
)

type Book struct {
    BookName string
    L        *sync.Mutex
}

func (bk *Book) SetName(wg *sync.WaitGroup, name string) {
    defer func() {
        fmt.Println("Unlock set name:", name)
        bk.L.Unlock()
        wg.Done()
    }()

    bk.L.Lock()
    fmt.Println("Lock set name:", name)
    time.Sleep(1 * time.Second)
    bk.BookName = name
}

func main() {
    bk := Book{}
    bk.L = new(sync.Mutex)
    wg := &sync.WaitGroup{}
    books := []string{"《三國演義》", "《道德經(jīng)》", "《西游記》"}
    for _, book := range books {
        wg.Add(1)
        go bk.SetName(wg, book)
    }

    wg.Wait()
}
程序輸出:

Lock set name: 《西游記》
Unlock set name: 《西游記》
Lock set name: 《三國演義》
Unlock set name: 《三國演義》
Lock set name: 《道德經(jīng)》
Unlock set name: 《道德經(jīng)》

23.2 讀寫鎖

讀寫鎖是分別針對讀操作和寫操作進行鎖定和解鎖操作的互斥鎖。在Go語言中辉浦,讀寫鎖由結(jié)構(gòu)體類型sync.RWMutex代表弄抬。

基本遵循原則:

  • 寫鎖定情況下,對讀寫鎖進行讀鎖定或者寫鎖定盏浙,都將阻塞眉睹;而且讀鎖與寫鎖之間是互斥的;

  • 讀鎖定情況下废膘,對讀寫鎖進行寫鎖定竹海,將阻塞;加讀鎖時不會阻塞丐黄;

  • 對未被寫鎖定的讀寫鎖進行寫解鎖斋配,會引發(fā)Panic;

  • 對未被讀鎖定的讀寫鎖進行讀解鎖的時候也會引發(fā)Panic灌闺;

  • 寫解鎖在進行的同時會試圖喚醒所有因進行讀鎖定而被阻塞的goroutine艰争;

  • 讀解鎖在進行的時候則會試圖喚醒一個因進行寫鎖定而被阻塞的goroutine。

與互斥鎖類似桂对,sync.RWMutex類型的零值就已經(jīng)是立即可用的讀寫鎖了甩卓。在此類型的方法集合中包含了兩對方法,即:

RWMutex提供四個方法:

func (*RWMutex) Lock // 寫鎖定
func (*RWMutex) Unlock // 寫解鎖

func (*RWMutex) RLock // 讀鎖定
func (*RWMutex) RUnlock // 讀解鎖

package main

import (
    "fmt"
    "sync"
    "time"
)

var m *sync.RWMutex

func main() {
    wg := sync.WaitGroup{}
    wg.Add(20)
    var rwMutex sync.RWMutex
    Data := 0
    for i := 0; i < 10; i++ {
        go func(t int) {
            rwMutex.RLock()
            defer rwMutex.RUnlock()
            fmt.Printf("Read data: %v\n", Data)
            wg.Done()
            time.Sleep(2 * time.Second)
            // 這句代碼第一次運行后蕉斜,讀解鎖逾柿。
            // 循環(huán)到第二個時,讀鎖定后宅此,這個goroutine就沒有阻塞机错,同時讀成功。
        }(i)

        go func(t int) {
            rwMutex.Lock()
            defer rwMutex.Unlock()
            Data += t
            fmt.Printf("Write Data: %v %d \n", Data, t)
            wg.Done() 

// 這句代碼讓寫鎖的效果顯示出來父腕,寫鎖定下是需要解鎖后才能寫的弱匪。
            time.Sleep(2 * time.Second)     
}(i)
    }
    time.Sleep(5 * time.Second)
    wg.Wait()
}

23.3 sync.WaitGroup

前面例子中我們有使用WaitGroup,它用于線程同步璧亮,WaitGroup等待一組線程集合完成萧诫,才會繼續(xù)向下執(zhí)行斥难。 主線程(goroutine)調(diào)用Add來設(shè)置等待的線程(goroutine)數(shù)量。 然后每個線程(goroutine)運行财搁,并在完成后調(diào)用Done蘸炸。 同時,Wait用來阻塞尖奔,直到所有線程(goroutine)完成才會向下執(zhí)行搭儒。Add(-1)和Done()效果一致。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(t int) {
            defer wg.Done()
            fmt.Println(t)
        }(i)
    }
    wg.Wait()
}

23.4 sync.Once

sync.Once.Do(f func())能保證once只執(zhí)行一次,這個sync.Once塊只會執(zhí)行一次提茁。

package main

import (
    "fmt"
    "sync"
    "time"
)

var once sync.Once

func main() {

    for i, v := range make([]string, 10) {
        once.Do(onces)
        fmt.Println("v:", v, "---i:", i)
    }

    for i := 0; i < 10; i++ {

        go func(i int) {
            once.Do(onced)
            fmt.Println(i)
        }(i)
    }
    time.Sleep(4000)
}
func onces() {
    fmt.Println("onces")
}
func onced() {
    fmt.Println("onced")
}

23.5 sync.Map

隨著Go1.9的發(fā)布淹禾,有了一個新的特性,那就是sync.map茴扁,它是原生支持并發(fā)安全的map铃岔。雖然說普通map并不是線程安全(或者說并發(fā)安全),但一般情況下我們還是使用它峭火,因為這足夠了毁习;只有在涉及到線程安全,再考慮sync.map卖丸。

但由于sync.Map的讀取并不是類型安全的纺且,所以我們在使用Load讀取數(shù)據(jù)的時候我們需要做類型轉(zhuǎn)換。

sync.Map的使用上和map有較大差異稍浆,詳情見代碼载碌。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    //Store
    m.Store("name", "Joe")
    m.Store("gender", "Male")

    //LoadOrStore
    //若key不存在,則存入key和value衅枫,返回false和輸入的value
    v, ok := m.LoadOrStore("name1", "Jim")
    fmt.Println(ok, v) //false Jim

    //若key已存在嫁艇,則返回true和key對應(yīng)的value,不會修改原來的value
    v, ok = m.LoadOrStore("name", "aaa")
    fmt.Println(ok, v) //true Joe

    //Load
    v, ok = m.Load("name")
    if ok {
        fmt.Println("key存在弦撩,值是: ", v)
    } else {
        fmt.Println("key不存在")
    }

    //Range
    //遍歷sync.Map
    f := func(k, v interface{}) bool {
        fmt.Println(k, v)
        return true
    }
    m.Range(f)

    //Delete
    m.Delete("name1")
    fmt.Println(m.Load("name1"))

}

本書《Go語言四十二章經(jīng)》內(nèi)容在github上同步地址:https://github.com/ffhelicopter/Go42
本書《Go語言四十二章經(jīng)》內(nèi)容在簡書同步地址: http://www.reibang.com/nb/29056963

雖然本書中例子都經(jīng)過實際運行步咪,但難免出現(xiàn)錯誤和不足之處,煩請您指出益楼;如有建議也歡迎交流歧斟。
聯(lián)系郵箱:roteman@163.com

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市偏形,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌觉鼻,老刑警劉巖俊扭,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異坠陈,居然都是意外死亡萨惑,警方通過查閱死者的電腦和手機捐康,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來庸蔼,“玉大人解总,你說我怎么就攤上這事〗憬觯” “怎么了花枫?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長掏膏。 經(jīng)常有香客問我劳翰,道長,這世上最難降的妖魔是什么馒疹? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任佳簸,我火速辦了婚禮,結(jié)果婚禮上颖变,老公的妹妹穿的比我還像新娘生均。我一直安慰自己,他們只是感情好腥刹,可當(dāng)我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布马胧。 她就那樣靜靜地躺著,像睡著了一般肛走。 火紅的嫁衣襯著肌膚如雪漓雅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天朽色,我揣著相機與錄音邻吞,去河邊找鬼。 笑死葫男,一個胖子當(dāng)著我的面吹牛抱冷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播梢褐,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼旺遮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了盈咳?” 一聲冷哼從身側(cè)響起耿眉,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎鱼响,沒想到半個月后鸣剪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年筐骇,在試婚紗的時候發(fā)現(xiàn)自己被綠了债鸡。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡铛纬,死狀恐怖厌均,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情告唆,我是刑警寧澤棺弊,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站悔详,受9級特大地震影響镊屎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茄螃,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一缝驳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧归苍,春花似錦用狱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吻氧,卻和暖如春溺忧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背盯孙。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工鲁森, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人振惰。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓歌溉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親骑晶。 傳聞我的和親對象是個殘疾皇子痛垛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容