Golang 併發程式設計

pardon110發表於2020-05-13

CSP(Communicating Sequential Processes) 通訊順序程式。用於描述併發通訊中的模式。golang原生的chan通道可阻塞,可鎖可佇列性。它的目的在於,保證併發執行的可控,即讓併發的無序執行在某些條件下具備有序可控。

Goroutine本身是非同步執行的程式碼塊,它的執行順序與其書寫分配 任務順序無關,與執行時通道,排程策略相關。

生產者 VS 消費者

通道阻塞與否事關後續程式碼的執行時機。通常同一個函式或goroutine內程式碼塊是同步順序執行的。可一旦內有通道阻塞,則意味著當前阻塞點後續程式碼的執行會在其他goroutine某段程式碼之後釋放(即依賴於另一個對應通道讀寫),換而言之,會發生執行上下文切換,而並非上一行阻塞點執行完畢,立刻執行下一行。表現出來的樣子,有小子在阻塞點位前插隊。

細思極恐,若不停的切換上下文,則存在多個goroutine中有程式碼段被阻塞的通道切割成了多個小片段,每次切換實際上只是執行區間小片段,然後再進入其它的Groutine。這有點更像goto,執行一段跳到另外一段,未來某時又跳回來執行後續程式碼。

chan 本質是有序,當它處於阻塞時,是指該程式碼點之後的程式碼執行,需要等待執行時觸發其狀態變更為非阻塞時方可執行。下述布林變數b在,done通道讀取之後時。其列印結果完全不同,決定了其布林值是輸出頭或尾。

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
    "runtime"
    "runtime/pprof"
)

type Consumer struct {
    msgs *chan int
}


func NewConsumer(msgs *chan int) *Consumer {
    return &Consumer{msgs: msgs}
}

// consumer 不停地讀取通道
func (c *Consumer) consume(){
    fmt.Println("consume: Started")
    for {
        msg := <-*c.msgs
        fmt.Println("consume: Received:", msg)
    }
}

// Producer 結構體
type Producer struct {
    msgs *chan int
    done *chan bool
}

func NewProducer(msgs *chan int, done *chan bool)*Producer {
    return &Producer{msgs: msgs, done:done}
}

// 生產者方法
func (p *Producer)produce(max int){
    fmt.Println("produce: Started")
    for i := 0; i < max; i++ {
        fmt.Println("produce:Sending ", i)
        *p.msgs <-i
    }
    // 阻塞在外部得到訊號退出 
    *p.done <- true
    fmt.Println("produce: Done")
}


func main() {
    cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file")    
    memprofile := flag.String("memprofile", "", "write memory profile to `file`")
    max := flag.Int("n", 5, "defines the number of messages")

    flag.Parse()

    runtime.GOMAXPROCS(runtime.NumCPU())

    if *cpuprofile != ""{
        f,err := os.Create(*cpuprofile)
        if err != nil{
            log.Fatal("could not create CPU profile: ", err)
        }
        if err := pprof.StartCPUProfile(f);err!=nil{
            log.Fatal("could not start CPU profile: ", err)
        }
        defer pprof.StopCPUProfile()
    }

    var msgs = make(chan int)
    var done = make(chan bool)

    // 使用結構體方法作為goroutine, 同一結構體資源使用必使用鎖(即chan),即使訪問具有有效性
    go NewProducer(&msgs,&done).produce(*max)
    go NewConsumer(&msgs).consume()

    var b bool
    fmt.Println(b)
    <-done

    if *memprofile != ""{
        f,err := os.Create(*memprofile)
        if err!= nil{
            log.Fatal("could not create memory profile: ", err)
        }
        runtime.GC()
        if err := pprof.WriteHeapProfile(f);err!=nil{
            log.Fatal("could not write memory profile: ", err)
        }
        f.Close()
    }
}

效果


D:\code-base\gomod\gott>go run "d:\code-base\gomod\gott\main.go"
false
consume: Started
produce: Started
produce:Sending  0
produce:Sending  1
consume: Received: 0
consume: Received: 1
produce:Sending  2
produce:Sending  3
consume: Received: 2
consume: Received: 3
produce:Sending  4
produce: Done

D:\code-base\gomod\gott>

死迴圈中的通道

之所以對通道說這麼多,是想講明不像通常所見,從上到下書寫執行。而是強調在goroutine中阻塞,會迅速切換上下文,並不會陷入consume方法看起來死迴圈(若無通道又非goroutine,該段程式碼是鐵定在理論上會耗盡cpu計算資源)。另外補充一下,每個goroutine的時間片是有時限。通道在某種意義上協調多個goroutine之間的執行

從另一方面也說明並非讀寫通道就會切換執行上下文,比如在無緩衝寫通道不會阻塞後續程式碼,有多次寫入未滿緩衝通道亦不會阻塞。所以無論是有緩衝通道,還是無緩衝通道,對於goroutine而言他上下文切換,通道阻塞是其觸發條件之一。

理髮師問題

理髮店在理髮室內有一張理髮椅,且在待理室有若干個供客戶等候休息的凳子。當完成理髮,理髮師不再伺候,直接進待理室看看是否還有其他待理髮的人,若無則返回理髮室睡覺。每當有客人進店,他會先瞅瞅理髮師在作什麼。若在睡則叫他起來幹活,若在理髮則在待理室等候,如果沒有凳子可供休息,則會選擇直接離開。

package main

import (
    "fmt"
    "sync"
    "time"
)

const(
    sleeping = iota
    checking
    cutting
)

var stateLog = map[int]string{
    0:"Sleeping",
    1:"Checking",
    2:"Cutting",
}

var wg *sync.WaitGroup // 潛在的客戶數

type Barber struct {
    name string
    sync.Mutex
    state int // Sleeping/Checking/Cutting
    customer *Customer
}

type Customer struct {
    name string
}

// 獲取當前客戶地址
func (c *Customer)String() string{
    return fmt.Sprintf("%p", c)[7:]
}

func NewBarber()(b *Barber){
    return &Barber{
        name: "Sam",
        state:sleeping,
    }
}

// 理髮師 goroutine
// 核查客戶
// 理髮師睡覺 等待喚醒
func barber(b *Barber, wr chan *Customer,wakers chan *Customer){
    for {
        b.Lock()
        defer b.Unlock()
        b.state = checking
        b.customer=nil

        // 檢查待理室客戶
        fmt.Printf("Checking waiting room: %d\n", len(wr))
        // 模擬查詢時阻塞
        time.Sleep(time.Microsecond*100)

        // 待理室分配任務
        select{
        case c := <-wr:
            // 1.主動理髮
            // 理髮
            HairCut(c,b)
            // 解鎖
            b.Unlock()
        default:
            // 2. 喚醒理髮
            // 待理室人員為空,列印理髮師名
            fmt.Printf("Sleeping Barber - %s \n", b.customer)
            // 重置理髮狀態
            b.state = sleeping
            b.customer = nil
            b.Unlock()
            // 喚醒者客戶出現
            c := <-wakers
            // 理髮時加鎖
            b.Lock()
            fmt.Printf("Woken by %s\n", c)
            HairCut(c,b)
            b.Unlock()
        }
    }
}

// 理髮邏輯,此段程式碼多個goroutine共用,注意加解鎖
func HairCut(c *Customer, b *Barber){
    b.state = cutting
    b.customer = c
    b.Unlock()

    fmt.Printf("Cutting  %s hair\n", c)
    // 模擬理髮時間
    time.Sleep(time.Millisecond * 100)
    b.Lock()
    wg.Done()
    // 使用者走人
    b.customer = nil
}

// 客戶 goroutine
// 若待理室已滿,則理髮失敗,否則進來的都是客戶
func customer(c *Customer, b *Barber, wr chan<- *Customer, wakers chan<- *Customer){
    // 客戶進來 
    time.Sleep(time.Microsecond*50)
    // 理髮上鎖
    b.Lock()
    // 當前使用者,理髮師狀態,待理室使用者數,具備喚醒權的使用者婁數,喚醒人
    fmt.Printf(" Customer %s checks %s barber | room: %d, w %d - customer: %s\n", c, stateLog[b.state],len(wr),len(wakers),b.customer)
    switch b.state {
    // 理髮師在睡覺
    case sleeping:
        select {
        // 客戶成為待喚醒者
        case wakers <- c:
        default:
            // 否則先成為待理室成員
            select{
            case wr <-c:
            default:
                // 待理室沒人,則預設理髮師完成工作
                wg.Done()
            }
        }
    case cutting:
        select{
            // 待理室有人
        case wr <-c:
        default:
            // 待理室已滿坐不下了,預設執行此多餘人離開理髮店
            wg.Done()
        }
    case checking:
        // 丟擲 客戶goroutine 不應核驗發師,應由理髮師核檢待理室的客戶
        panic("Customer shouldn't check for the Barber when Barber is Checking the waiting room")
    }
    b.Unlock()
}

func main() {
    b := NewBarber()
    b.name = "Pardon"
    // 模擬待理室有5把椅子
    WaitingRoom := make(chan *Customer, 5)
    // 存在一位喚醒者
    Wakers := make(chan *Customer,1)

    // 理髮師處理
    go barber(b, WaitingRoom, Wakers)

    time.Sleep(time.Microsecond*100)
    wg = new(sync.WaitGroup)
    n := 10
    // 增加10個計數點
    wg.Add(10)

    // 生成10個客戶
    for i := 0; i < n; i++ {
        time.Sleep(time.Microsecond*50)
        c := new(Customer)
        // 分配十個消費goroutine
        go customer(c, b, WaitingRoom, Wakers)
    }

    // 阻塞主執行緒
    wg.Wait()
    fmt.Println("No more customers for the day")
}

效果

D:\code-base\gomod\gott>go run "d:\code-base\gomod\gott\main.go"
Checking waiting room: 0
Sleeping Barber - <nil>
 Customer 32010 checks Sleeping barber | room: 0, w 0 - customer: <nil>
Woken by 32010
Cutting  32010 hair
 Customer 32020 checks Cutting barber | room: 0, w 0 - customer: 32010
 Customer 4c1f0 checks Cutting barber | room: 1, w 0 - customer: 32010
 Customer 32040 checks Cutting barber | room: 2, w 0 - customer: 32010
 Customer 32050 checks Cutting barber | room: 3, w 0 - customer: 32010
 Customer 32060 checks Cutting barber | room: 4, w 0 - customer: 32010
 Customer 4c220 checks Cutting barber | room: 5, w 0 - customer: 32010
 Customer 4c230 checks Cutting barber | room: 5, w 0 - customer: 32010
 Customer e2000 checks Cutting barber | room: 5, w 0 - customer: 32010
 Customer c0060 checks Cutting barber | room: 5, w 0 - customer: 32010
Checking waiting room: 5
Cutting  32020 hair
Checking waiting room: 4
Cutting  4c1f0 hair
Checking waiting room: 3
Cutting  32040 hair
Checking waiting room: 2
Cutting  32050 hair
Checking waiting room: 1
Cutting  32060 hair
Checking waiting room: 0
No more customers for the day

D:\code-base\gomod\gott>cls

小結

前者基於結構體的方法,後者直接用函式作為gortouine。二者在訪問共用邏輯時注意加鎖,多個goroutine執行通過chan來協作。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章