Golang併發:除了channel,你還有其餘選擇

咱們都知道Golang併發優選channel,但channel不是萬能的,Golang爲咱們提供了另外一種選擇:sync。經過這篇文章,你會了解sync包最基礎、最經常使用的方法,至於sync和channel之爭留給下一篇文章。git

sync包提供了基礎的異步操做方法,好比互斥鎖(Mutex)、單次執行(Once)和等待組(WaitGroup),這些異步操做主要是爲低級庫提供,上層的異步/併發操做最好選用通道和通訊。github

sync包提供了:golang

  1. Mutex:互斥鎖
  2. RWMutex:讀寫鎖
  3. WaitGroup:等待組
  4. Once:單次執行
  5. Cond:信號量
  6. Pool:臨時對象池
  7. Map:自帶鎖的map

這篇文章是sync包的入門文章,因此只介紹經常使用的結構和方法:MutexRWMutexWaitGroupOnce,而CondPoolMap留給你們自行探索,或有需求再介紹。bash

互斥鎖

常作併發工做的朋友對互斥鎖應該不陌生,Golang裏互斥鎖須要確保的是某段時間內,不能有多個協程同時訪問一段代碼(臨界區)微信

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

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

Mutex的初始值爲未鎖的狀態,而且Mutex一般做爲結構體的匿名成員存在less

通過了上面這麼「官方」的介紹,舉個例子:你在工商銀行有100元存款,這張卡綁定了支付寶和微信,在中午12點你用支付寶支付外賣30元,你在微信發紅包,搶到10塊。銀行須要按順序執行上面兩件事,先減30再加10或者先加10再減30,結果都是80,但若是同時執行,結果多是,只減了30或者只加了10,即你有70元或者你有110元。前一個結果是你賠了,後一個結果是銀行賠了,銀行可不但願把這種事算錯。異步

看看實際使用吧:建立一個銀行,銀行裏存每一個帳戶的錢,存儲查詢都加了鎖操做,這樣銀行就不會算錯帳了。
銀行的定義:函數

type Bank struct {
    sync.Mutex
    saving map[string]int // 每帳戶的存款金額
}

func NewBank() *Bank {
    b := &Bank{
        saving: make(map[string]int),
    }
    return b
}

銀行的存取錢:測試

// Deposit 存款
func (b *Bank) Deposit(name string, amount int) {
    b.Lock()
    defer b.Unlock()

    if _, ok := b.saving[name]; !ok {
        b.saving[name] = 0
    }
    b.saving[name] += amount
}

// Withdraw 取款,返回實際取到的金額
func (b *Bank) Withdraw(name string, amount int) int {
    b.Lock()
    defer b.Unlock()

    if _, ok := b.saving[name]; !ok {
        return 0
    }
    if b.saving[name] < amount {
        amount = b.saving[name]
    }
    b.saving[name] -= amount

    return amount
}

// Query 查詢餘額
func (b *Bank) Query(name string) int {
    b.Lock()
    defer b.Unlock()

    if _, ok := b.saving[name]; !ok {
        return 0
    }

    return b.saving[name]
}

模擬操做:小米支付寶存了100,而且同時花了20。

func main() {
    b := NewBank()
    go b.Deposit("xiaoming", 100)
    go b.Withdraw("xiaoming", 20)
    go b.Deposit("xiaogang", 2000)

    time.Sleep(time.Second)
    fmt.Printf("xiaoming has: %d\n", b.Query("xiaoming"))
    fmt.Printf("xiaogang has: %d\n", b.Query("xiaogang"))
}

結果:先存後花。

➜  sync_pkg git:(master) ✗ go run mutex.go
xiaoming has: 80
xiaogang has: 2000

也多是:先花後存,由於先花20,由於小明沒錢,因此沒花出去。

➜  sync_pkg git:(master) ✗ go run mutex.go
xiaoming has: 100
xiaogang has: 2000

這個例子只是介紹了mutex的基本使用,若是你想多研究下mutex,那就去個人Github(閱讀原文)下載下來代碼,本身修改測試。Github中還提供了沒有鎖的例子,運行屢次總能碰到錯誤:

fatal error: concurrent map writes
這是因爲併發訪問map形成的。

讀寫鎖

讀寫鎖是互斥鎖的特殊變種,若是是計算機基本知識紮實的朋友會知道,讀寫鎖來自於讀者和寫者的問題,這個問題就不介紹了,介紹下咱們的重點:讀寫鎖要達到的效果是同一時間能夠容許多個協程讀數據,但只能有且只有1個協程寫數據

也就是說,讀和寫是互斥的,寫和寫也是互斥的,但讀和讀並不互斥。具體講,當有至少1個協程讀時,若是須要進行寫,就必須等待全部已經在讀的協程結束讀操做,寫操做的協程纔得到鎖進行寫數據。當寫數據的協程已經在進行時,有其餘協程須要進行讀或者寫,就必須等待已經在寫的協程結束寫操做。

讀寫鎖是RWMutex,它有5個函數,它須要爲讀操做和寫操做分別提供鎖操做,這樣就4個了:

  • 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(){}

上面的銀行實現不合理:你們都是拿手機APP查餘額,能夠同時幾我的一塊兒查呀,這根本不影響,銀行的鎖能夠換成讀寫鎖。存、取錢是寫操做,查詢金額是讀操做,代碼修改以下,其餘不變:

type Bank struct {
    sync.RWMutex
    saving map[string]int // 每帳戶的存款金額
}

// Query 查詢餘額
func (b *Bank) Query(name string) int {
    b.RLock()
    defer b.RUnlock()

    if _, ok := b.saving[name]; !ok {
        return 0
    }

    return b.saving[name]
}

func main() {
    b := NewBank()
    go b.Deposit("xiaoming", 100)
    go b.Withdraw("xiaoming", 20)
    go b.Deposit("xiaogang", 2000)

    time.Sleep(time.Second)
    print := func(name string) {
        fmt.Printf("%s has: %d\n", name, b.Query(name))
    }

    nameList := []string{"xiaoming", "xiaogang", "xiaohong", "xiaozhang"}
    for _, name := range nameList {
        go print(name)
    }

    time.Sleep(time.Second)
}

結果,可能不同,由於協程都是併發執行的,執行順序不固定

➜  sync_pkg git:(master) ✗ go run rwmutex.go
xiaohong has: 0
xiaozhang has: 0
xiaogang has: 2000
xiaoming has: 100

等待組

互斥鎖和讀寫鎖大多數人可能比較熟悉,而對等待組(WaitGroup)可能就不那麼熟悉,甚至有點陌生,因此先來介紹下等待組在現實中的例子。

大家團隊有5我的,你做爲隊長要帶領你們打開藏有寶藏的箱子,但這個箱子須要4把鑰匙才能同時打開,你把尋找4把鑰匙的任務,分配給4個隊員,讓他們分別去尋找,而你則守着寶箱,在這等待,等他們都找到回來後,一塊兒插進鑰匙打開寶箱。

這其中有個很重要的過程叫等待:等待一些工做完成後,再進行下一步的工做。若是使用Golang實現,就得使用等待組。

等待組是WaitGroup,它有3個函數:

  • Add():在被等待的協程啓動前加1,表明要等待1個協程。
  • Done():被等待的協程執行Done,表明該協程已經完成任務,通知等待協程。
  • Wait(): 等待其餘協程的協程,使用Wait進行等待。
type WaitGroup
func (wg *WaitGroup) Add(delta int){}
func (wg *WaitGroup) Done(){}
func (wg *WaitGroup) Wait(){}

來,一塊兒看下怎麼用WaitGroup實現上面的問題。

隊長先建立一個WaitGroup對象wg,每一個隊員都是1個協程, 隊長讓隊員出發前,使用wg.Add(),隊員出發尋找鑰匙,隊長使用wg.Wait()等待(阻塞)全部隊員完成,某個隊員完成時執行wg.Done(),等全部隊員找到鑰匙,wg.Wait()則返回,完成了等待的過程,接下來就是開箱。

結合以前的協程池的例子,修改爲WG等待協程池協程退出,實例代碼:

func leader() {
    var wg sync.WaitGroup
    wg.Add(4)
    for i := 0; i < 4; i++ {
        go follower(&wg, i)
    }
    wg.Wait()
    
    fmt.Println("open the box together")
}

func follower(wg *sync.WaitGroup, id int) {
    fmt.Printf("follwer %d find key\n", id)
    wg.Done()
}

結果:

➜  sync_pkg git:(master) ✗ go run waitgroup.go
follwer 3 find key
follwer 1 find key
follwer 0 find key
follwer 2 find key
open the box together

WaitGroup也經常使用在協程池的處理上,協程池等待全部協程退出,把上篇文章《Golang併發模型:輕鬆入門協程池》的例子改下:

func workerPool(n int, jobCh <-chan int, retCh chan<- string) {
    var wg sync.WaitGroup
    wg.Add(n)
    for i := 0; i < n; i++ {
        go worker(&wg, i, jobCh, retCh)
    }

    wg.Wait()
    close(retCh)
}

func worker(wg *sync.WaitGroup, id int, jobCh <-chan int, retCh chan<- string) {
    cnt := 0
    for job := range jobCh {
        cnt++
        ret := fmt.Sprintf("worker %d processed job: %d, it's the %dth processed by me.", id, job, cnt)
        retCh <- ret
    }

    wg.Done()
}

單次執行

在程序執行前,一般須要作一些初始化操做,但觸發初始化操做的地方是有多處的,可是這個初始化又只能執行1次,怎麼辦呢?

使用Once就能輕鬆解決,once對象是用來存放1個無入參無返回值的函數,once能夠確保這個函數只被執行1次

type Once
func (o *Once) Do(f func()){}

直接把官方代碼給你們搬過來看下,once在10個協程中調用,但once中的函數onceBody()只執行了1次:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var once sync.Once
    onceBody := func() {
        fmt.Println("Only once")
    }
    done := make(chan bool)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody)
            done <- true
        }()
    }
    for i := 0; i < 10; i++ {
        <-done
    }
}

結果:

➜  sync_pkg git:(master) ✗ go run once.go
Only once

示例源碼

本文全部示例源碼,及歷史文章、代碼都存儲在Github:https://github.com/Shitaibin/golang_step_by_step/tree/master/sync_pkg

下期預告

此次先介紹入門的知識,下次再介紹一些深刻思考、最佳實踐,不能一口吃個胖子,我們慢慢來,順序漸進。

下一篇我以這些主題進行介紹,歡迎關注:

  1. 哪一個協程先獲取鎖
  2. 必定要用鎖嗎
  3. 鎖與通道的選擇

文章推薦

  1. Golang併發模型:輕鬆入門流水線模型
  2. Golang併發模型:輕鬆入門流水線FAN模式
  3. Golang併發模型:併發協程的優雅退出
  4. Golang併發模型:輕鬆入門select
  5. Golang併發模型:select進階
  6. Golang併發模型:輕鬆入門協程池
  7. Golang併發的次優選擇:sync包
  1. 若是這篇文章對你有幫助,請點個贊/喜歡,感謝
  2. 本文做者:大彬
  3. 若是喜歡本文,隨意轉載,但請保留此原文連接:http://lessisbetter.site/2019/01/04/golang-pkg-sync/

一塊兒學Golang-分享有料的Go語言技術

相關文章
相關標籤/搜索