一、線程安全介紹
1.1 現實例子
A. 多個goroutine同時操作一個資源,這個資源又叫臨界區
B. 現實生活中的十字路口,通過紅路燈實現線程安全
C. 火車上的廁所(進去之后先加鎖,在上廁所,不加鎖兩個人都進去就出問題了,出來后在解鎖,別人就可以使用了),通過互斥鎖來實現線程安全
D、在程序中,同一個變量多個goroutine去修改的時候,肯定是不允許同時修改的,同時修改肯定會出問題,所以當一個goroutine在修改之前需要加鎖,修改結束在解鎖,這樣別的goroutine就可以去修改了。
1.2 實際例子
x = x +1
A. 先從內存中取出x的值
B. CPU進行計算, x+1
C. 然后把x+1的結果存儲在內存中
解釋:
就是兩個goroutine同時去操作x(共享資源),最后的結果x並不是2,由於線程安全的問題,導致最后的結果還是等於1;
詳情也如下圖所示:
下面來看一個實際例子:
test1和test2函數都是在自增到1000000(對同一個變量count進行修改)
1)當test1函數和test2函數跑在同一個線程時:
package main import ( "fmt" ) var count int func test1() { for i := 0; i < 1000000; i++ { count++ } } func test2() { for i := 0; i < 1000000; i++ { count++ } } func main() { test1() test2() fmt.Printf("count=%d\n", count) }
執行結果如下:
因為是串行執行,所以最終結果肯定是2000000
2)當test1函數和test2函數獨自起goroutine運行時:
package main import ( "fmt" "time" ) var count int func test1() { for i := 0; i < 1000000; i++ { count++ } } func test2() { for i := 0; i < 1000000; i++ { count++ } } func main() { go test1() go test2() time.Sleep(time.Second) fmt.Printf("count=%d\n", count) }
執行結果如下:
解釋:
可以看到當test1和test2同時運行對count(共享資源)進行修改時,就會出現沖突,最終結果也就不是2000000了
1.3 如何解決?
那么如何解決上述線程安全問題呢,就是我們接下來要學習的互斥鎖。
第2章 互斥鎖
2.1 互斥鎖介紹
A. 同時有且只有一個線程進入臨界區,其他的線程則在等待鎖;
B. 當互斥鎖釋放之后,等待鎖的線程才可以獲取鎖進入臨界區;
C. 多個線程同時等待同一個鎖,喚醒的策略是隨機的;
2.2 互斥鎖使用實例
package main import ( "fmt" "sync" //互斥鎖需要使用這個包。 "time" ) var count int var mutex sync.Mutex //定義一個鎖的變量(互斥鎖的關鍵字是Mutex,其是一個結構體,傳參一定要傳地址,否則就不對了) func test1() { for i := 0; i < 1000000; i++ { mutex.Lock() //對共享變量操作之前先加鎖 count++ mutex.Unlock() //對共享變量操作完畢在解鎖,這樣就保護了共享的資源 } } func test2() { for i := 0; i < 1000000; i++ { mutex.Lock() count++ mutex.Unlock() } } func main() { go test1() go test2() time.Sleep(time.Second) fmt.Printf("count=%d\n", count) }
執行結果如下:
解釋:
加鎖(互斥鎖)之后其實是相當於串行(對共享變量進行操作時)執行了,就算是goroutine也不例外。
2.3 互斥鎖高階實例
1)未加互斥鎖代碼(有問題)
package main import ( "fmt" "sync" ) var x = 0 func increment(wg *sync.WaitGroup) { x = x + 1 wg.Done() } func main() { var w sync.WaitGroup for i := 0; i < 1000; i++ { w.Add(1) go increment(&w) } w.Wait() fmt.Println("final value of x", x) }
執行結果:
2)添加互斥鎖代碼
package main import ( "fmt" "sync" ) var x = 0 func increment(wg *sync.WaitGroup, m *sync.Mutex) { m.Lock() x = x + 1 m.Unlock() wg.Done() } func main() { var w sync.WaitGroup var m sync.Mutex for i := 0; i < 1000; i++ { w.Add(1) go increment(&w, &m) } w.Wait() fmt.Println("final value of x", x) }
執行結果:
三、讀寫鎖
3.1 使用場景
A. 讀多寫少的場景;
B. 分為兩種角色,讀鎖和寫鎖;
C. 當一個goroutine獲取寫鎖之后,其他的goroutine獲取寫鎖或讀鎖都會等待;
D. 當一個goroutine獲取讀鎖之后,其他的goroutine獲取寫鎖都會等待, 但其他
goroutine獲取讀鎖時,都會繼續獲得鎖.;
3.2 讀寫鎖案例演示
package main import ( "sync" "time" ) var rwlock sync.RWMutex //定義一個鎖的變量(讀寫鎖的關鍵字是RWMutex,其是一個結構體,傳參一定要傳地址,否則就不對了) var wg sync.WaitGroup var count int func writer() { //寫的線程 for i := 0; i < 1000; i++ { // 加寫鎖 rwlock.Lock() //加鎖寫鎖之后,其他goroutine就不能針對該共享變量加讀鎖或寫鎖(讀取或寫入)了 count++ time.Sleep(10 * time.Millisecond) //模擬寫操作需要10ms // 釋放寫鎖 rwlock.Unlock() } wg.Done() } func reader() { //讀的線程 for i := 0; i < 1000; i++ { // 加讀鎖 rwlock.RLock() //對於讀鎖來說,其他goroutine依然可以對該共享變量進行讀取(讀鎖)依然可以,但是寫入不行,獲取寫鎖需要等待。 _ = count //fmt.Printf("count=%d\n", count) time.Sleep(1 * time.Millisecond) //模擬讀操作場景需要1ms // 釋放讀鎖 rwlock.RUnlock() } wg.Done() } func main() { wg.Add(1) go writer() for i := 0; i < 10; i++ { wg.Add(1) go reader() //讀鎖是並發的,這里加了for循環主要是為了模擬只要有1個goroutine能夠讀取到共享資源,其他的goroutine也可以獲取到。 } wg.Wait() }
執行結果:
3.3 讀寫鎖和互斥鎖性能比較
針對同一個程序,我們通過比較互斥鎖和讀寫鎖的耗時來進行直觀展示:
首先計算讀寫鎖性能:
代碼示例如下:
package main import ( "fmt" "sync" "time" ) var rwlock sync.RWMutex //定義一個鎖的變量(讀寫鎖的關鍵字是RWMutex,其是一個結構體,傳參一定要傳地址,否則就不對了) var wg sync.WaitGroup var count int func writer() { //寫的線程 for i := 0; i < 1000; i++ { // 加寫鎖 rwlock.Lock() //加鎖寫鎖之后,其他goroutine就不能針對該共享變量加讀鎖或寫鎖(讀取或寫入)了 count++ time.Sleep(10 * time.Millisecond) //模擬寫操作需要10ms // 釋放寫鎖 rwlock.Unlock() } wg.Done() } func reader() { //讀的線程 for i := 0; i < 1000; i++ { // 加讀鎖 rwlock.RLock() //對於讀鎖來說,其他goroutine依然可以對該共享變量進行讀取(讀鎖)依然可以,但是寫入不行,獲取寫鎖需要等待。 _ = count //fmt.Printf("count=%d\n", count) time.Sleep(1 * time.Millisecond) //模擬讀操作場景需要1ms // 釋放讀鎖 rwlock.RUnlock() } wg.Done() } func main() { start := time.Now().UnixNano() //開始時間 wg.Add(1) go writer() for i := 0; i < 10; i++ { wg.Add(1) go reader() //讀鎖是並發的,這里加了for循環主要是為了模擬只要有1個goroutine能夠讀取到共享資源,其他的goroutine也可以獲取到。 } wg.Wait() end := time.Now().UnixNano() //結束時間 cost := (end - start) / 1000 / 1000 / 1000 fmt.Printf("cost %d s\n", cost) }
執行結果如下:
互斥鎖性能:
見如下實例:
package main import ( "fmt" "sync" "time" ) var mlock sync.Mutex //聲明互斥鎖變量 var wg sync.WaitGroup var count int func writer_mutex() { //寫的線程 for i := 0; i < 1000; i++ { mlock.Lock() count++ time.Sleep(10 * time.Millisecond) //模擬寫操作需要10ms mlock.Unlock() } wg.Done() } func reader_mutex() { //讀的線程 for i := 0; i < 1000; i++ { mlock.Lock() //對於多個goroutine來說,互斥鎖也是只有1個goroutine可以讀,並不像讀寫鎖一樣,所有goroutine都可以讀 _ = count //fmt.Printf("count=%d\n", count) time.Sleep(1 * time.Millisecond) //模擬讀操作場景需要1ms mlock.Unlock() } wg.Done() } func main() { start := time.Now().UnixNano() //開始時間 wg.Add(1) go writer_mutex() for i := 0; i < 10; i++ { wg.Add(1) go reader_mutex() } wg.Wait() end := time.Now().UnixNano() //結束時間 cost := (end - start) / 1000 / 1000 / 1000 fmt.Printf("cost %d s\n", cost) }
執行結果如下:
總結:
可以看到最終的結果是同一個程序互斥鎖比讀寫鎖耗時多了9秒,主要原因是在讀的時候,讀寫鎖可以多個讀線程去讀,而互斥鎖依然只能是一個線程去讀,1比10的比例,就造成了最終這個結果。
葵花寶典:
讀多寫少用讀寫鎖,讀寫差不多用互斥鎖。