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

一、兩個問題

1、同步執(zhí)行問題
package main

import (
    "fmt"
    "time"
)

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

func fun1() {
    fmt.Println("fun1函數(shù)執(zhí)行")
}

func fun2() {
    fmt.Println("fun2函數(shù)執(zhí)行")
}

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

2、臨界資源問題

臨界資源: 指并發(fā)環(huán)境中多個進程/線程/協(xié)程共享的資源。并發(fā)編程中對臨界資源的處理不當, 往往會導致數(shù)據(jù)不一致的問題。

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

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

package main

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

//全局變量票數(shù)
var tickets = 10

func main() {

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

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

func saleTickets(name string) {
    //隨機數(shù)種子
    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 售罄,已無票。。

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

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

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

goroutine同步方式

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

二、sync同步等待組WaitGroup

使用等待組進行多個任務的同步,等待組可以保證在并發(fā)環(huán)境中完成指定數(shù)量的任務。
等待組的方法:

方法名 功能
(wg *WaitGroup)Add(delta int) 等待組的計數(shù)器+1
(wg *WaitGroup)Done() 等待組的計數(shù)器-1
(wg *WaitGroup)Wait() 當?shù)却M計數(shù)器不等于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函數(shù)等待")
    wg.Wait()
    fmt.Println("main函數(shù)結束")
}

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

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

運行結果

main函數(shù)等待
fun1函數(shù)執(zhí)行
fun2函數(shù)執(zhí)行
main函數(shù)結束

三、sync互斥鎖Mutex

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

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

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

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

package main

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

//全局變量票數(shù)
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) {
    //隨機數(shù)種子
    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

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

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

讀寫鎖是RWMutex,它有5個函數(shù):

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

舉個例子,學生信息錄入系統(tǒng),錄入學生信息是寫操作,讀取學生信息是讀操作??梢允褂米x寫鎖:

package main

import (
    "fmt"
    "sync"
)

var wg sync.WaitGroup

// Student 學生信息系統(tǒng)
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單次執(zhí)行Once

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

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

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

代碼示例

package main

import (
    "sync"
)

var configs map[string]string

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

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

var loadConfigOnce sync.Once

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

func main() {

}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容