Go語言學習7:併發

併發與並行

go是支持併發的語言。

併發的程序往往是在單核中交替運行的(通信開銷小),並行的程序往往是多核同時運行的(通信開銷大)。
在這裏插入圖片描述

Go 協程(Goroutine)

Go 協程是與其他函數或方法一起併發運行的函數或方法。Go 協程可以看作是輕量級線程。與線程相比,創建一個 Go 協程的成本很小。

package main

import (  
    "fmt"
    "time"
)

func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

go hello() 啓動了一個新的 Go 協程。現在 hello() 函數與 main() 函數會併發地執行。

啓動一個新的協程時,協程的調用會立即返回。與函數不同,程序控制不會去等待 Go 協程執行完畢。在調用 Go 協程之後,程序控制會立即返回到代碼的下一行,忽略該協程的任何返回值。

主函數會運行在一個特有的 Go 協程上,它稱爲 Go 主協程(Main Goroutine)。如果 Go 主協程終止,則程序終止,於是其他 Go 協程也不會繼續運行。

信道(channel)

信道來實現 Go 協程間的通信。

package main

import (  
    "fmt"
)

func hello(done chan bool) {  
    fmt.Println("Hello world goroutine")
    done <- true
}
func main() {  
    done := make(chan bool)
    // var done chan bool
    go hello(done)
    data := <-done
    // 也可以不賦值直接丟棄結果
    // <-done
    fmt.Println("main function")
}

信道是一種引用類型(還有slice、map、interface{}),可以通過make聲明。所有信道都關聯了一個類型。信道只能運輸這種類型的數據。

上述例子中,done <- true表示把值發送進信道中,data := <-done表示從信道中接收值。

發送和接收默認是阻塞的。當把數據發送到信道時,程序會在發送數據的語句處發生阻塞,直到有其它 Go 協程從信道讀取到數據,纔會解除阻塞。與此類似,當讀取信道的數據時,如果沒有其它的協程把數據寫入到這個信道,那麼讀取過程就會一直阻塞着。

單向信道

前邊所說的信道都是雙向信道,既能發送數據,又能接收數據。其實也可以創建單向信道,這種信道只能發送或者接收數據。

只不過一個不能讀取數據的唯送信道究竟有什麼意義呢?所以把一個雙向信道轉換成唯送信道或者唯收(Receive Only)信道都是行得通的,但是反過來就不行。

package main

import "fmt"

func sendData(sendch chan<- int) {  
	// 參數如果是xxx <-chan int表示單向接收
    sendch <- 10
}

func main() {  
    cha1 := make(chan int)
    go sendData(cha1)
    fmt.Println(<-cha1)
}

關閉信道和使用 for range 遍歷信道

數據發送方可以用close()函數關閉信道,通知接收方這個信道不再有數據發送過來。

package main

import (  
    "fmt"
)

func producer(chnl chan int) {  
    for i := 0; i < 10; i++ {
        chnl <- i
    }
    close(chnl)
}
func main() {  
    ch := make(chan int)
    go producer(ch)
    for v := range ch {
        fmt.Println("Received ",v)
    }
}

for range 循環用於在一個信道關閉之前,從信道接收數據。或者接收方也可以多用一個變量來檢查信道是否已經關閉:v, ok := <- ch

緩衝信道

之前我們討論的都是無緩衝信道,它的發送和接收過程是阻塞的。

我們還可以創建一個有緩衝(Buffer)的信道。只在緩衝已滿的情況,纔會阻塞向緩衝信道(Buffered Channel)發送數據。同樣,只有在緩衝爲空的時候,纔會阻塞從緩衝信道接收數據。

// 要讓一個信道有緩衝,capacity 應該大於 0。
ch := make(chan type, capacity)

sync.WaitGroup

WaitGroup 用於等待一批 Go 協程執行結束。程序控制會一直阻塞,直到這些協程全部執行完畢。假設我們有 3 個併發執行的 Go 協程(由 Go 主協程生成)。Go 主協程需要等待這 3 個協程執行結束後,纔會終止。這就可以用 sync.WaitGroup 來實現。

package main

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

func process(i int, wg *sync.WaitGroup) {  
    fmt.Println("started Goroutine ", i)
    time.Sleep(2 * time.Second)
    fmt.Printf("Goroutine %d ended\n", i)
    wg.Done()
}

func main() {  
    no := 3
    var wg sync.WaitGroup
    for i := 0; i < no; i++ {
        wg.Add(1)
        go process(i, &wg)
    }
    wg.Wait()
    fmt.Println("All go routines finished executing")
}

WaitGroup 是一個結構體類型,使用計數器來工作,其初始值爲零值。當我們調用 WaitGroup 的 Add()方法 並傳遞一個 int 時,WaitGroup 的計數器會加上 Add 的傳參。要減少計數器,可以調用 WaitGroup 的 Done() 方法。Wait() 方法會阻塞調用它的 Go 協程,直到計數器變爲 0 後纔會停止阻塞。

需要注意,傳遞 wg 的地址是很重要的。如果沒有傳遞 wg 的地址,那麼每個 Go 協程將會得到一個 WaitGroup 值的拷貝,因而當它們執行結束時,main 函數並不會知道。

select

select 語句用於在多個發送/接收信道操作中進行選擇。select 語句會一直阻塞,直到發送/接收操作準備就緒。如果有多個信道操作準備完畢,select 會隨機地選取其中之一執行。該語法與 switch 類似。

package main

import (  
    "fmt"
    "time"
)

func server1(ch chan string) {  
    time.Sleep(6 * time.Second)
    ch <- "from server1"
}
func server2(ch chan string) {  
    time.Sleep(3 * time.Second)
    ch <- "from server2"

}
func main() {  
    output1 := make(chan string)
    output2 := make(chan string)
    go server1(output1)
    go server2(output2)
    select {
    case s1 := <-output1:
        fmt.Println(s1)
    case s2 := <-output2:
        fmt.Println(s2)
    default:
        fmt.Println("default")
    }
}

sync.Mutex

當程序併發地運行時,多個 Go 協程不應該同時訪問那些修改共享資源的代碼。這些修改共享資源的代碼稱爲臨界區

互斥鎖(Mutex)通過Lock()Unlock()可以保證在任意時刻只允許一個 Go 協程訪問臨界區。如果有一個 Go 協程已經持有了鎖(Lock),當其他協程試圖獲得該鎖時,這些協程會被阻塞,直到 Mutex 解除鎖定爲止。

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)
}

注意也是傳遞地址。


學習資料:https://studygolang.com/subject/2

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章