【go語言學習】標準庫之sync

一、兩個問題

1、同步執行問題
package main

import (
    "fmt"
    "time"
)

func main() {
    go fun1()
    go fun2()
    fmt.Println("main函數等待")
    time.Sleep(time.Second * 1)
    fmt.Println("main函數結束")
}

func fun1() {
    fmt.Println("fun1函數執行")
}

func fun2() {
    fmt.Println("fun2函數執行")
}

主線程為了等待所有的子goroutine都運行完畢,不得不在程序中使用time.Sleep() 來睡眠一段時間,等待其他線程充分運行。這種方式耗費時間,顯然是不夠優雅的。

2、臨界資源問題

臨界資源: 指并發環境中多個進程/線程/協程共享的資源。并發編程中對臨界資源的處理不當, 往往會導致數據不一致的問題。

如果多個goroutine在訪問同一個數據資源(臨界資源)的時候,其中一個線程修改了數據,那么這個數值就被修改了,對于其他的goroutine來講,這個數值可能是不對的。

舉個例子,我們通過并發來實現火車站售票這個程序。一共有10張票,3個售票口同時出售。

package main

import (
    "fmt"
    "math/rand"
    "time"
)

//全局變量票數
var tickets = 10

func main() {

    //三個goroutine  模擬售票窗口
    go saleTickets("售票口1")
    go saleTickets("售票口2")
    go saleTickets("售票口3")

    //為了保證3個goroutine協程正常工作,先將主線程睡眠5秒
    time.Sleep(5 * time.Second)
}

func saleTickets(name string) {
    //隨機數種子
    rand.Seed(time.Now().UnixNano())
    for {
        if tickets > 0 {
            //隨機睡眠1~1000ms
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            fmt.Println(name, "余票:", tickets)
            tickets--
        } else {
            fmt.Println(name, "售罄,已無票。。")
            break
        }
    }
}

運行結果

售票口3 余票: 10
售票口2 余票: 10
售票口1 余票: 10
售票口3 余票: 7
售票口1 余票: 7
售票口3 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口2 余票: 3
售票口1 余票: 3
售票口1 售罄,已無票。。
售票口2 余票: 0
售票口2 售罄,已無票。。
售票口3 余票: -1
售票口3 售罄,已無票。。

在以上的代碼中,使用三個并發運行的go協程模擬了三個售票窗口同時售票,而由于全局變量tickets會被三個協程在一段時間內同時訪問,因此tickets就是我們所說的“臨界資源”。
我們可以發現:

在開始時,三個窗口同時讀到信息:tickets=10,從而隨機都輸出了余票=10
而在結尾時,竟然出現了余票為負數的情況,其產生的原因在于,票數快要賣完時,當售票口1余票1,并且售完這一張票后,在這個時間段內,售票口2已經進入了if tickets > 0滿足條件的代碼塊內,然而售票口1此時將最后一張票售出,tickets 由1變為0售票口2打印出來了不應該出現的結果:余票0,同理售票口3打印了不該出現的結果:余票-1。

多goroutine【多任務】,有共享資源,且多goroutine修改共享資源,出現數據不安全問題【數據錯誤】,保證數據安全一致,需要goroutine同步

goroutine同步方式

  • channel 【csp模型】
  • sync包提供的方法

二、sync同步等待組WaitGroup

使用等待組進行多個任務的同步,等待組可以保證在并發環境中完成指定數量的任務。
等待組的方法:

方法名 功能
(wg *WaitGroup)Add(delta int) 等待組的計數器+1
(wg *WaitGroup)Done() 等待組的計數器-1
(wg *WaitGroup)Wait() 當等待組計數器不等于0時阻塞,直到為0

代碼示例:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

func main() {
    wg.Add(1)
    go fun1()
    wg.Add(1)
    go fun2()
    fmt.Println("main函數等待")
    wg.Wait()
    fmt.Println("main函數結束")
}

func fun1() {
    fmt.Println("fun1函數執行")
    wg.Done()
}

func fun2() {
    fmt.Println("fun2函數執行")
    wg.Done()
}

運行結果

main函數等待
fun1函數執行
fun2函數執行
main函數結束

三、sync互斥鎖Mutex

加鎖成功則操作資源,加鎖失敗則等待直至鎖加鎖成功——所有的goroutine互斥,一個得到鎖其他全部等待。

互斥鎖被稱為Mutex,它有2個函數,Lock()和Unlock()分別是獲取鎖和釋放鎖,如下:

type Mutex
func (m *Mutex) Lock(){}
func (m *Mutex) Unlock(){}

修改上面售票代碼,解決臨界資源安全問題
示例代碼:

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

//全局變量票數
var tickets = 10
var mutex sync.Mutex
var wg sync.WaitGroup

func main() {

    //三個goroutine  模擬售票窗口
    wg.Add(1)
    go saleTickets("售票口1")
    wg.Add(1)
    go saleTickets("售票口2")
    wg.Add(1)
    go saleTickets("售票口3")
    wg.Wait()
}

func saleTickets(name string) {
    //隨機數種子
    rand.Seed(time.Now().UnixNano())
    for {
        //上鎖
        mutex.Lock()
        if tickets > 0 {
            //隨機睡眠1~1000ms
            time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
            fmt.Println(name, "余票:", tickets)
            tickets--
        } else {
            mutex.Unlock()
            fmt.Println(name, "售罄,已無票。。")
            break

        }
        //解鎖
        mutex.Unlock()
    }
    wg.Done()
}

運行結果

售票口3 余票: 10
售票口3 余票: 9
售票口1 余票: 8
售票口2 余票: 7
售票口3 余票: 6
售票口1 余票: 5
售票口2 余票: 4
售票口3 余票: 3
售票口1 余票: 2
售票口2 余票: 1
售票口1 售罄,已無票。。
售票口2 售罄,已無票。。
售票口3 售罄,已無票。。

四、sync讀寫鎖RWMutex

讀寫鎖要達到的效果是同一時間可以允許多個協程讀數據,但只能有且只有1個協程寫數據。也就是說,讀和寫是互斥的,寫和寫也是互斥的,但讀和讀并不互斥。
簡單來說:

  • (1)可以隨便讀,多個goroutine同時讀。讀的時候不能寫。
  • (2)寫的時候,啥也不能干。不能讀也不能寫。

讀寫鎖是RWMutex,它有5個函數:

  • Lock()和Unlock()是給寫操作用的。
  • RLock()和RUnlock()是給讀操作用的。
  • RLocker()能獲取讀鎖,然后傳遞給其他協程使用。使用較少。
type RWMutex
func (rw *RWMutex) Lock(){}
func (rw *RWMutex) RLock(){}
func (rw *RWMutex) RLocker() Locker{}
func (rw *RWMutex) RUnlock(){}
func (rw *RWMutex) Unlock(){}

舉個例子,學生信息錄入系統,錄入學生信息是寫操作,讀取學生信息是讀操作。可以使用讀寫鎖:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

// Student 學生信息系統
type Student struct {
    // 讀寫鎖
    sync.RWMutex
    // 存儲信息 姓名-年齡
    data map[string]int
}

// Add 增加學生信息
func (s *Student) Add(name string, age int) {
    defer wg.Done()
    s.Lock()
    defer s.Unlock()
    if _, ok := s.data[name]; !ok {
        s.data[name] = age
    }
}

// Query 讀取學生信息
func (s *Student) Query(name string) {
    defer wg.Done()
    s.RLock()
    defer s.RUnlock()
    if v, ok := s.data[name]; ok {
        fmt.Printf("姓名:%s\t年齡:%d\n", name, v)
    } else {
        fmt.Println("學生信息不存在!")
    }
}

func main() {
    s := &Student{
        data: make(map[string]int),
    }
    wg.Add(4)
    s.Add("jack", 20)
    s.Add("tom", 23)
    s.Add("lili", 18)
    s.Add("lili", 20)

    nameList := []string{"jack", "tom", "lili", "xiaohua"}
    for _, v := range nameList {
        wg.Add(1)
        go s.Query(v)
    }
    wg.Wait()
}

運行結果

學生信息不存在!
姓名:jack      年齡:20
姓名:lili      年齡:18
姓名:tom       年齡:23

五、sync單次執行Once

sync.Once 是 Golang package 中使方法只執行一次的對象實現,作用與 init 函數類似。但也有所不同:

  • init 函數是在文件包首次被加載的時候執行,且只執行一次
  • sync.Once 是在代碼運行中需要的時候執行,且只執行一次

當一個函數不希望程序在一開始的時候就被執行的時候,我們可以使用 sync.Once 。
sync.Once是讓函數方法只被調用執行一次的實現,其最常應用于單例模式之下,例如初始化系統配置、保持數據庫唯一連接等。

代碼示例

package main

import (
    "sync"
)

var configs map[string]string

func loadConfig() {
    configs = map[string]string{
        "url":   "http://www.lxweimin.com",
        "id":    "cd41c8c3645c",
        "email": "everydawn@jianshu.com",
    }
}

// Config1 被多個goroutine調用時不是并發安全的
// 比如有兩個線程都在調用Config1函數,線程A在執行到if configs==nil后
// cpu切換到線程B執行,直到線程B運行完,這時configs已經被實例化,
// 當cpu在切回到線程A繼續執行的時候,對configs又執行實例化操作,
// 這時內存中已有configs的兩個實例,違背了單例定義。
func Config1(name string) string {
    if configs == nil {
        loadConfig()
    }
    return configs[name]
}

var loadConfigOnce sync.Once

// Config2 是并發安全的
func Config2(name string) string {
    loadConfigOnce.Do(loadConfig)
    return configs[name]
}

func main() {

}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。