《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