「Golang成長之路」併發之Channel上

ice_moss發表於2021-10-04

一、使用channel等待任務結束

在前面的內容中很多地方使用到了:
time.Sleep(time.Millisecond)
如:

func chanDemo(){
    var channels [10]chan int  //建立channel陣列
    for i := 0; i < 10; i++{
        channels[i] = creatworker(i)
    }
    for i := 0; i < 10; i++{
        channels[i] <- 'a' + 1
    }
    time.Sleep(time.Millisecond)
}
func bufferedChannel(){
    ch := make(chan int, 3)
    go worker(0, ch)
    for i := 0; i < 10; i++{
        ch <- 'a' + i
    }
    time.Sleep(time.Millisecond)
}
func channelClose(){
    ch := make(chan int)
    go worker(0, ch)
    ch <- 'a'
    ch <- 'b'
    ch <- 'c'
    ch <- 'd'
    close(ch)
    time.Sleep(time.Millisecond)
}
在這些方法裡面我們很容易知道他們執行所消耗的時間,但事實上,很多程式的時間是不能預估的,我們不能一直是用time包來對程式執行的時間進行預估,是不靠譜的,所以這裡我們有了“使用channel等待任務結束”

先看這段程式碼:

func chanDemo(){
   var channels [10]chan int //建立channel陣列
   for i := 0; i < 10; i++{
      channels[i] = creatworker(i)
   }
   for i := 0; i < 10; i++{
      channels[i] <- 'a' + 1
  }
   time.Sleep(time.Millisecond)
}

我們需要使用channel併發的列印10個字母,為了讓字母完整列印,我們對程式執行時間進行了預估,讓程式執行1毫秒就結束;下面我們需要使用
1.使用channel等待任務結束
仍然列印字母(列印20個):部分內容見程式碼註釋
使用’chan bool’的通道來共享通訊來使用記憶體,告訴main任務結束

package main

import (
   "fmt"
)
//定義一個結構體
//包含一個 'chan int ' 的in和 'chan bool'的done
type Worker struct{
   in chan int
   done chan bool  //對done的接和收作為結束的訊號
}

func DoWork( id int, c chan int, done chan bool){
   for {
      n := <-c //接受channel的內容
      fmt.Printf("worker %d received %c\n", id, n)
      done <- true  //done接收資料true
  }
}

func createWorker(id int) Worker {
   //建立Worker的一個物件w
   w := Worker{make(chan int),
   make(chan bool),
   }

   go DoWork(id, w.in, w.done) 此處啟動goroutine,即併發
   return w
}

func ChanDemo() {

   var workers [10]Worker  //建立10個抽象型別Worker

  for i := 0; i < 10; i++{ 
      workers[i] = createWorker(i) //建立10個Worker的物件,並返回給workers[i]
   }

   for i := 0; i < 10; i++{  //可使用range
      workers[i].in <- 'a' + i   //workers[i].in接受資料
   }

   for _, worker := range workers {
      <-worker.done   //將done的資料發給mian,告知main該任務結束
   }

   for i := 0; i < 10; i++{ //可使用range
      workers[i].in <- 'A'+ i  //workers[i].in接受資料
   }

   for _, worker := range workers{
      <- worker.done  //將done的資料發給mian,告知main該任務結束
   }
}

func main(){
   ChanDemo()
}

列印結果:
worker 0 received a
worker 5 received f
worker 1 received b
worker 6 received g
worker 4 received e
worker 9 received j
worker 8 received i
worker 2 received c
worker 7 received h
worker 3 received d
worker 6 received G
worker 2 received C
worker 3 received D
worker 7 received H
worker 1 received B
worker 4 received E
worker 5 received F
worker 0 received A
worker 9 received J
worker 8 received I

從列印結果看出:是按順序列印的(先小寫後大寫)
這裡還有一種方法:

func ChanDemo() {

   var workers [10]Worker

  for i := 0; i < 10; i++{
      workers[i] = createWorker(i)
   }
   for i := 0; i < 10; i++{
      workers[i].in <- 'a' + i
   }
   for i := 0; i < 10; i++{
      workers[i].in <- 'A'+ i
   }
   //將兩個<- worker.done 放在一起
   for _, worker := range workers{
      <- worker.done   
      <- worker.done
   }
}

但是需要注意的是:
我們一共建立了10個goroutine,在第二個for中就已經向所有channel中傳送資料,接著第三個for,又向channel中傳送資料,這樣會死鎖,因為第一次channel中的資料沒有人來接,然後又向channel發資料。
解決方法:在DoWork函式增加併發,讓done <- true處於併發執行狀態,可隨時向main發資料。

func DoWork( id int, c chan int, done chan bool){
   for {
      n := <-c //接受channel的內容
      fmt.Printf("worker %d received %c\n", id, n)
      go func() {
        done <- true
     }()
  }
}

2.使用系統提供的 ‘WaitGroup’等待任務結束
WaitGroup提供了:Add()、Wait()、Done()方法

package main

import (
   "fmt"
 "sync")

type Worker struct{
   in chan int
   wg *sync.WaitGroup  //引用需要指標
}

func DoWork( id int, c chan int, wg *sync.WaitGroup){
   for {
      n := <-c //接受channel的內容
      fmt.Printf("worker %d received %c\n", id, n)
       wg.Done()  //接和收結束資訊
   }
}

func createWorker(id int, wg *sync.WaitGroup) Worker {
   //建立Worker的一個物件w
   w := Worker{make(chan int), wg,}

   go DoWork(id, w.in, wg)
   return w
}

func ChanDemo() {
   var wg sync.WaitGroup

   var workers [10]Worker
   for i := 0; i < 10; i++{
      workers[i] = createWorker(i, &wg)  //指標
   }
   wg.Add(20)  //20個任務
   for i := 0; i < 10; i++{
      workers[i].in <- 'a' + i
   }
   for i := 0; i < 10; i++{
      workers[i].in <- 'A'+ i
   }
   wg.Wait()  //任務結束
}

func main(){
   ChanDemo()
}

列印結果:
worker 0 received a
worker 4 received e
worker 6 received g
worker 2 received c
worker 9 received j
worker 7 received h
worker 0 received A
worker 8 received i
worker 5 received f
worker 3 received d
worker 1 received b
worker 1 received B
worker 9 received J
worker 2 received C
worker 3 received D
worker 4 received E
worker 7 received H
worker 8 received I
worker 6 received G
worker 5 received F

二、select

之前的內容中,我們使用channel都是一個一個的收資料,如果我們需要把多個channel同時收,該怎麼辦?
答案是:Go語言引入了select語句

下面來具體介紹一下select:
select的邏輯和switch的邏輯類似,他們都有多個case分支和default,但select是針對channel的,其邏輯是:在多個含有case分支的select裡面,當某時刻相應的channel滿足發發出資料,讓外面接收,就能滿足對應case,接下來就會執行該case對應的語句塊,如果多個case同時都滿足條件,則會隨機選擇其中一個case,如果所有case都不滿足則會執行default
例如:

var activeWorker chan<- int
n := 0
select {
  //c1, c2 為chan int型別
  case n = <-c1:
     fmt.Printf("this is c1:%d\n", n)
  case n = <-c2:
     fmt.Printf("this is c2:%d\n", n)
  case activeWorker <- n:
     hasValue = false
  default:
     fmt.Println("not find channel")
    return
  }

在執行select時,程式會將所有的case分析一遍,先來看第一個case,如果此時c1發出資料,則第一個case可被執行,再看第二個case,如果此時c2發出資料,則第二個case可被執行,再看第三個case,如果此時n有值就會將其值發給activeWorker,最後來看default,當上面所有的case都不滿足時,就會執行default的語句塊。

下面來看一個完整的select的應用:

package main

import (
   "fmt"
 "math/rand" "time")

//控制時間,向channel裡面傳送訊息
func generator() chan int{
   out := make(chan int)
   go func() {
      i := 0
     for{
         //控制傳送資料時間間隔
         time.Sleep(time.Duration( rand.Intn(1500) ) * time.Microsecond)
         out <- i
         i++
      }
   }()
   return out
}

//channel接受和列印資訊
func DoWork( id int, c chan int){
   for {
    n := <- c  //接受channel的內容
    time.Sleep(time.Second)  //控制列印時間間隔
    fmt.Printf("worker %d received %d\n", id, n)
   }
}

//建立channel,啟動併發
func createWorker(id int) chan<- int{
   w := make(chan int)
   go DoWork(id, w)
   return w
}

func main() {
   var c1, c2 = generator(), generator()
   var worker = createWorker(0)
   n := 0
   var Values []int //動態快取資料
   //tm為程式總時間  tm := time.After(1 * time.Second)
   tick := time.Tick(time.Second)

   for{
      var activeWorker chan<- int
      var activeValue int
      if len(Values) > 0 {
          activeWorker = worker
          activeValue = Values[0]
      }
      select {
      case n = <-c1:
         Values = append(Values, n)
      case n = <-c2:
         Values = append(Values, n)
      case activeWorker <- activeValue:
         Values = Values[1:]
      case <- time.After(2000 * time.Microsecond):  //每兩次傳送資料時間差超過800毫秒執行一次
         fmt.Println("timeout")
      case <- tick:   //使用tick反映系統狀態
         fmt.Println("queue len is:",len(Values))
      case <- tm:  //使用tm控制總時間
         fmt.Println("bey")
         return
     }
   }
}

列印結果:
worker 0 received 132
worker 0 received 133
worker 0 received 134
worker 0 received 136
worker 0 received 135
worker 0 received 136
worker 0 received 137
worker 0 received 138
worker 0 received 137
worker 0 received 138
worker 0 received 139
timeout
worker 0 received 140
worker 0 received 139
timeout
worker 0 received 140
worker 0 received 141
worker 0 received 142
worker 0 received 143
worker 0 received 141
timeout
worker 0 received 142
worker 0 received 144
worker 0 received 145
worker 0 received 143
worker 0 received 144
worker 0 received 145
worker 0 received 146
worker 0 received 146
worker 0 received 147
worker 0 received 147
worker 0 received 148
worker 0 received 148
worker 0 received 149
worker 0 received 149
worker 0 received 150
worker 0 received 151
timeout
worker 0 received 152
worker 0 received 150
worker 0 received 153
worker 0 received 151
worker 0 received 152
worker 0 received 153
worker 0 received 154
worker 0 received 154
timeout
worker 0 received 155
worker 0 received 156
worker 0 received 155
worker 0 received 157
worker 0 received 156
worker 0 received 158
worker 0 received 157
timeout
worker 0 received 158
worker 0 received 159
worker 0 received 159
worker 0 received 160
worker 0 received 161
worker 0 received 162
worker 0 received 160
timeout
worker 0 received 161
worker 0 received 163
worker 0 received 162
timeout
worker 0 received 164
worker 0 received 163
worker 0 received 164
worker 0 received 165
worker 0 received 166
worker 0 received 167
worker 0 received 165
worker 0 received 168
timeout
worker 0 received 166
worker 0 received 169
worker 0 received 167
worker 0 received 170
timeout
worker 0 received 171
worker 0 received 168
timeout
worker 0 received 172
worker 0 received 169
worker 0 received 170
worker 0 received 171
worker 0 received 173
timeout
worker 0 received 174
worker 0 received 172
worker 0 received 175
……
……
……
worker 0 received 2352
worker 0 received 2386
timeout
worker 0 received 2387
worker 0 received 2353
timeout
worker 0 received 2388
worker 0 received 2389
worker 0 received 2354
worker 0 received 2390
worker 0 received 2355
bey
這個程式充分體現了select的實際應用

三、在這裡總結了幾個常見的問題:

func ChanDemo() {

var workers [10]Worker

  for i := 0; i < 10; i++{
workers[i] = createWorker(i)
}

for i := 0; i < 10; i++{
workers[i].in <- 'a' + i
}

在第一個for中,第一步workers[0] = createWorker(0)

然後就進入這裡

func createWorker(id int) Worker {
//建立Worker的一個物件w
  w := Worker{make(chan int),
     make(chan bool),
     }

go DoWork(id, w.in, w.done)
return w
}

在這個函式中我們開了一個goroutine,同時我們會將w返回給workers[0],然後就進入:

for i := 0; i < 10; i++{
workers[i] = createWorker(i)
}

的第二次,第三次迴圈……

直到迴圈結束。

但是這裡就有問題了,在這個途中我們一共開了10 goroutine,但是這10 goroutine都處於等待狀態(因為我們還沒有給channel任何內容,從我們的輸出結果可以看出)

  1. 那麼這裡的10個goroutine是處於等待狀態是不是因為,我們channel沒有接受到任何資訊,所以就會造成goroutine的等待?

2. 還有這裡:

 func DoWork( id int, c chan int, done chan bool){
    for {
         n := <-c //接受channel的內容
         fmt.Printf("id: %v, chan:%c\n", id, n)
         done <- true
      }
    }

這個死迴圈,為什麼在函式呼叫後只迴圈了一遍? 當然這裡我知道他是其中一個goroutine

3. 當然還有一個問題,就是我們在前兩個問題在基礎上,呼叫函式DoWork()時,也會對應的將true傳送給與之對應的workers[i].done中,然後:

for i := 0; i < 10; i++{
workers[i].in <- 'a' + i
}

for _, worker := range workers {
<- worker.done
}

在這裡的第二個for中,這裡<- worker.done全為true,我們是不是從這裡就可以瞭解到前面的10個goroutine結束了?

4. 也正是因為這樣我們才不需要time包,來預計程式的執行時間了?

三、回答
  1. 是的,它們此時都在等待,等別人從in中傳送任務資料。

  2. 這是個死迴圈,一般我們goroutine中常會這麼寫,只要有任務就做。影片裡實際上大寫字母,小寫字母,一共執行兩遍。執行多少遍取決於外界,這裡是main函式,到底傳送了多少任務給我這個worker[i]。

  3. 這裡的true方向同學搞錯了,是worker通知main函式,說我做完了。<- worker.done這裡是main函式接收worker.done的資料,如果收到,就說明這個worker的事情做完了。

  4. ​是的,理想情況下應該不需要time包來預計執行時間。預計的時間會不靠譜。

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

相關文章