Golang 語言中 Channel 的使用方式

介紹

在「Effective Go」併發章節講到,“不要通過共享內存進行通信;而是通過通信共享內存”。由此表明 Golang 語言官方鼓勵用戶使用 “通過通信共享內存” 的方式併發編程。

但是,Golang 語言也在標準庫 sync 包中提供了傳統的同步原語。

我們應該選擇哪種併發編程方式呢?Golang 語言官方也給了使用指南:

如上所示,傳遞數據的所有權,分發工作任務和通信異步結果,這三種場景建議使用 Channel。緩存和狀態,這兩種場景建議使用 Mutex。

在 Golang 語言官方博客「The Go Memory Model」一文中,對 channel 的描述如下:

A send on a channel happens before the corresponding receive from that channel completes.
A receive from an unbuffered channel happens before the send on that channel completes.

本文我們主要介紹一些關於 Channel 的使用方式。關於 Golang 語言 sync 包的同步原語和 Channel 的介紹,我們在之前的文章已經介紹過了,在此不再贅述,感興趣的讀者朋友可以按需翻閱。

無緩衝 channel

無緩衝 channel 可用於兩個 goroutine 之間傳遞信號,比如以下示例:

順序打印 1 至 100 的奇數和偶數:

func main () {
 block := make(chan struct{})
 go odd(block)
 go even(block)
 time.Sleep(time.Second)
 fmt.Println("done")
}

func odd (block chan struct{}) {
 for i := 1; i <= 100; i++ {
  <-block
  if i % 2 == 1 {
   fmt.Println("奇數:", i)
  }
 }
}

func even (block chan struct{}) {
 for i := 1; i <= 100; i++ {
  block <- struct{}{}
  if i % 2 == 0 {
   fmt.Println("偶數:", i)
  }
 }
}

閱讀上面這段代碼,我們使用一個無緩衝 channel 作爲兩個 goroutine 之間的信號傳遞的橋樑。

主子 goroutine 之間傳遞信號:

func main () {
 block := make(chan struct{})
 go func() {
  for i := 0; i < 10; i++ {
   fmt.Println(i)
  }
   // block <- struct{}{}
    close(block)
 }()
 <-block
 fmt.Println("done")
}

閱讀上面這段代碼,我們使用一個無緩衝 channel 作爲主子 goroutine 之間的信號傳遞的橋樑。通過信號傳遞,主 goroutine 在子 goroutine 運行結束之後再退出。

有緩衝 channel

有緩衝 channel 可以用於解耦操作,模擬消息隊列。“生產者”和 “消費者” 只需各自處理 channel,實現解耦。

解耦 “生產者” 和“消費者”:

func main () {
 task := make(chan int, 10)
 go consumer(task)
 // 生產者
 for i := 0; i < 10; i++ {
  task <- i
 }
 time.Sleep(time.Second * 2)
}

// 消費者
func consumer (task <-chan int) {
 for i := 0; i < 10; i++ {
  go func(id int) {
   t := <- task
   fmt.Println(id, t)
  }(i)
 }
}

閱讀上面這段代碼,我們使用一個有緩衝的 channel,將 “生產者” 和“消費者”做解耦操作。

超時操作和定時器

我們還可以通過 select 和 channel,實現超時操作和定時器。

超時操作:

func main() {
    c1 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c1 <- "result 1"
    }()

    select {
    case res := <-c1:
        fmt.Println(res)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout 1")
    }

    c2 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "result 2"
    }()
    select {
    case res := <-c2:
        fmt.Println(res)
    case <-time.After(3 * time.Second):
        fmt.Println("timeout 2")
    }
}

閱讀上面這段代碼,我們通過 c1 和 c2 兩個 channel,分別模擬出超時和未超時場景。

定時器:

func main() {
    ticker := time.NewTicker(500 * time.Millisecond)
    done := make(chan bool)

    go func() {
        for {
            select {
            case <-done:
                return
            case t := <-ticker.C:
                fmt.Println("Tick at", t)
            }
        }
    }()

    time.Sleep(1600 * time.Millisecond)
    ticker.Stop()
    done <- true
    fmt.Println("Ticker stopped")
}

閱讀上面這段代碼,我們定義一個打點器,每間隔 500ms 執行一次操作,當打點器 stop 時,通過一個無緩衝 channel 傳遞退出信號。

總結

本文我們介紹了一些關於 Channel 的使用方式,我們在閱讀完本文後可以瞭解無緩衝 channel 作爲信號傳遞的使用方式和有緩衝 channel 解耦操作的方式,以及 channel 與 select 配合使用的用法。

參考資料:
https://golang.org/doc/effective_go#concurrency 
https://github.com/golang/go/wiki/MutexOrChannel 
https://tip.golang.org/ref/mem#tmp_3 
https://gobyexample.com/timeouts 
https://gobyexample.com/tickers

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/vXt0gqnxPK1qbChzZxV94A