Go語言基礎之13--線程安全及互斥鎖和讀寫鎖


一、線程安全介紹

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的比例,就造成了最終這個結果。

 

葵花寶典

讀多寫少用讀寫鎖,讀寫差不多用互斥鎖。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM