Go語言學習筆記 - 并發

Goroutine

Go在語言層面對并發編程提供支持,采用輕量級線程(協程)實現。只需要在函數調用語句前添加go關鍵字,就可以創建并發執行單元。開發人員無需了解任何執行細節,調度器會自動將其安排到合適的系統線程上執行。goroutine是一種非常輕量級的實現,可在單個進程里執行成千上萬的并發任務。事實上,入口函數main就以goroutine運行。另有與之配套的channel類型,用以實現“以通訊來共享內存”的CSP模式。

go func() {
    println("Hello, World!")
}

調度器不能保證多個goroutine執行次序,且進程退出時不會等待它們結束。默認情況下,進程啟動后僅允許一個系統線程服務于goroutine。可使用環境變量或標準函數runtime.GOMAXPROCS修改(Go 1.5默認方式)。讓高度器用多個線程實現多核并行,而不僅僅是并發。

func sum(id int) {
    var x int64
    for i := 0; i < math.MaxUint32; i++ {
        x += int64(i)
    }
    println(id, x)
}
func main() {
    wg := new(sync.WaitGroup)
    wg.Add(2)
    for i := 0; i < 2; i++ {
        go func(id int) {
            defer wg.Done()
            sum(id)
        }(i)
    }
    wg.Wait()
}
輸出:
$ go build -o test
$ time -p ./test
0 9223372030412324865
1 9223372030412324865
real 7.70 // 程序開始到結束時間差 (非非 CPU 時間)
user 7.66 // 用用戶態所使用用 CPU 時間片片 (多核累加)
sys 0.01 // 內核態所使用用 CPU 時間片片
$ GOMAXPROCS=2 time -p ./test
0 9223372030412324865
1 9223372030412324865
real 4.18
user 7.61// 雖然總時間差不多,但由 2 個核并行行,real 時間自自然少了許多。
sys 0.02

調用 runtime.Goexit 將立即終止當前 goroutine 執行,調度器確保所有已注冊 defer延遲調用被執行。

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(1)
    go func() {
        defer wg.Done()
        defer println("A.defer")
        func() {
            defer println("B.defer")
            runtime.Goexit() // 終止止當前 goroutine
            println("B") // 不會執行行
        }()
        println("A")    // 不會執行行
    }()
    wg.Wait()
}
//輸出:
B.defer
A.defer

和協程 yield 作用類似,Gosched 讓出底層線程,將當前 goroutine 暫停,放回隊列等待下次被調度執行。

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(2)
    go func() {
        defer wg.Done()
        for i := 0; i < 6; i++ {
            println(i)
            if i == 3 { runtime.Gosched() }
        }
    }()
    go func() {
        defer wg.Done()
        println("Hello, World!")
    }()
    wg.Wait()
}
//輸出
$ go run main.go
0
1
2
3
Hello, World!
4
5

Channel

引用類型Channel是CSP模型的具體實現,用于多個goruntine之間進行通訊。其內部實現了同步,確保了并發安全。默認為同步模式,需要發送和接收配對。否則會被阻塞,直到另一方準備好后被喚醒。

func main() {
    data := make(chan int) // 數據交換隊列
    exit := make(chan bool) // 退出通知
    go func() {
        for d := range data {// 從隊列迭代接收數據,直到 close 。
            fmt.Println(d)
        }
        fmt.Println("recv over.")
        exit <- true// 發出退出通知。
    }()
    data <- 1// 發送數據。
    data <- 2
    data <- 3
    close(data)// 關閉隊列。
    fmt.Println("send over.")
    <-exit// 等待退出通知。
}
//輸出:
1
2
3
send over.
recv over.

異步方式通過判斷緩沖區來決定是否阻塞。如果緩沖區已滿,發送被阻塞;緩沖區為空,接收被阻塞。通常情況下,異步channel可減少排隊阻塞,具備更高的效率。但應該考慮使用指針規避大對象拷貝,將多個元素打包,減少緩沖區大小等。

func main() {
    data := make(chan int, 3)// 緩沖區可以存儲 3 個元素
    exit := make(chan bool)
    data <- 1// 在緩沖區未滿前,不會阻塞。
    data <- 2
    data <- 3
    go func() {
        for d := range data {// 在緩沖區未空前,不會阻塞。
            fmt.Println(d)
        }
        exit <- true
    }()
    data <- 4// 如果緩沖區已滿,阻塞。
    data <- 5
    close(data)
    <-exit
}

緩沖區是內部屬性,并非類型構成要素。

var a, b chan int = make(chan int), make(chan int, 3)

除用用 range 外,還可用 ok-idiom 模式判斷 channel 是否關閉。

for {
    if d, ok := <-data; ok {
        fmt.Println(d)
    } else {
        break
    }
}

向 closed channel 發送數據引發 panic 錯誤,接收立即返回零值。而 nil channel,無論收發都會被阻塞。內置函數 len 返回未被讀取的緩沖元素數量,cap 返回緩沖區大小。

單向

可以將channel隱式轉換為單身隊列,只收或只發。

c := make(chan int, 3)
var send chan <- int = c // send only
var recv <- chan int = c // receiver only

選擇

如果需要同時處理多個channel,可以用select語句,它隨機選擇一個可用的channel做收發操作,或執行default case。

func main() {
    a, b := make(chan int, 3), make(chan int)
    go func() {
        v, ok, s := 0, false, ""
        for {
            select {// 隨機選擇可用用 channel,接收數據。
            case v, ok = <-a: s = "a"
            case v, ok = <-b: s = "b"
            }
            if ok {
              fmt.Println(s, v)
            } else {
                os.Exit(0)
            }
        }
    }()
    for i := 0; i < 5; i++ {
          select {// 隨機選擇可用用 channel,發送數據。
          case a <- i:
          case b <- i:
          }
    }
    close(a)
    select {}// 沒有可用用 channel,阻塞 main goroutine。
}
//輸出:
b 3
a 0
a 1
a 2
b 4

模式

用簡單工廠模式打包并發任務和channel。

func NewTest() chan int {
    c := make(chan int)
    rand.Seed(time.Now().UnixNano())
    go func() {
        time.Sleep(time.Second)
        c <- rant.Int()
    }()
    return c
}
func main() {
    t := NewTest()
    println(<-t) //等待gorountime結束返回。
}

用channel實現信號量(semaphore)。

func main() {
    wg := sync.WaitGroup{}
    wg.Add(3)
    sem := make(chan int, 1)
    for i := 0; i < 3; i++ {
        go func(id int) {
            defer wg.Done()
            sem <- 1 // 向 sem 發送數據,阻塞或者成功。
            for x := 0; x < 3; x++ {
                fmt.Println(id, x)
            }
            <- sem // 接收數據,使得其他阻塞 goroutine 可以發送數據。
        }(i)
    }
    wg.Wait()
}

用closed channel發出退出通知。

func main() {
    var wg sync.WaitGroup
    quit := make(chan bool)
    for i := 0; i < 2; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            task := func() {
                println(id, time.Now().Nanosecond())
                time.Sleep(time.Second)
            }
            for {
              select {
              case <- quit: // closed channel 不會阻塞,因此可用作退出通知。
                  return
              default://執行正常任務
                  task()
              }
          }
        }(i)
    }
    time.Sleep(time.Second * 5) // 讓測試 goroutine 運行一會。
    close(quit) // 發出退出通知。
    wg.Wait()
}

用select 實現超時 (timeout)。channel 是第一類對象,可傳參 (內部實現為指針) 或者作為結構成員。

type Request struct {
    data []int
    ret chan int
}
func NewRequest(data ...int) *Request {
    return &Request{ data, make(chan int, 1) }
}
func Process(req *Request) {
    x := 0
    for _, i := range req.data {
        x += i
    }
    req.ret <- x
}
func main() {
    req := NewRequest(10, 20, 30)
    Process(req)
    fmt.Println(<-req.ret)
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • GO并發使用goroutine運行程序,檢測并修正狀態,利用通道共享數據。通常程序會被編寫為一個順序執行并完成一個...
    小線亮亮閱讀 566評論 0 1
  • Goroutine是Go里的一種輕量級線程——協程。相對線程,協程的優勢就在于它非常輕量級,進行上下文切換的代價非...
    witchiman閱讀 4,902評論 0 9
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,372評論 11 349
  • 今天介紹一下 go語言的并發機制以及它所使用的CSP并發模型 CSP并發模型 CSP模型是上個世紀七十年代提出的,...
    falm閱讀 68,654評論 10 80
  • *什么時候可以再次見到輕松氛圍* 什么時候知道自己的成長 什么時候失去了自己 什么時候,我能找回來?
    章魚農夫閱讀 185評論 0 1