go : channel , queue , 程式管理 , 關閉channel ?

滾球獸進化發表於2022-07-15

今天看到一篇有關channel的問答。文章中中提到了channel的快取區,當時我看到快取區的反應是 是不是可以把我之前寫的佇列用 channel

進行替換。隨著 channel 的研究,發現水很深。相對於不要透過共享記憶體來通訊,要透過通訊來共享記憶體這句看似簡單的話,使用起來存在的問題還是不少的。

這是一段我曾經寫的 channel 使用程式碼存在有瑕疵。因為使用的是無緩衝通道,所以不容易發現。


func  main(){

cInt := make(chan  int, 0) // 無緩衝通道

endInt := make(chan  int)

go  func() {

for  i := 1; i <= 10; i++ {

fmt.Println(i, "wait into channel")

cInt <- i

}

// close(cInt)

endInt <- 1

}()

write := func() {

for {

select {

case  data := <-cInt:

fmt.Println("get ", data, " from channel")

time.Sleep(time.Millisecond * 500)

break

case <-endInt:

return

}

}

}

write()

}

如果把緩衝區變得比較大,那麼問題就凸顯出來了


cInt := make(chan  int, 10)

write() 提前結束了,並沒有堅持到最後。


···

9 wait into channel

10 wait into channel

get 1 from channel

get 2 from channel

get 3 from channel

# end

原因就是生產端同時生產 cInt ,endInt 。接受方希望透過 endInt 判斷結束,但是 select 的消費順序是無序的。也就是說在 cInt ,endInt 均有資料的時候,並不是優先消費cInt。一旦消費endInt 就跳出了整體,從而拋棄了還未處理的資料。

當前場景的問題修復是比較簡單的,可以在生產端在資料生產完畢後 close 這個 channel, 對於消費方可以改用以下帶啊嗎進行退出操作。


for  data := range cInt {

fmt.Println(data)

}

range 會在所有資料讀取完畢後跳出迴圈。現在已經修復完畢了,那麼可以在這個基礎上使用 channel 作為一個任務分發的佇列呢。比如爬蟲的後續任務的入隊,和當前任務的分發。我的觀點是,也不是不可以,但是有侷限性。可以看一下下面的程式碼,受限於 channel 長度的影響,下述程式碼會發生死鎖。


func  channelSpider(cmd *cobra.Command, args []string) {

cInt := make(chan  int, 10)

// 模擬爬取的資料

cInt <- 1

spider := func(i int) {

// todo spider

// push next job

nestJobNum := rand.Intn(3)

for  i := 0; i <= nestJobNum; i++ {

cInt <- i

}

}

for  i := 1; i <= 3; i++ {

go  func() {

for  cData := range cInt {

fmt.Println("開始消費")

spider(cData)

fmt.Println("消費結束")

}

}()

}

time.Sleep(time.Second * 100)

}

對於上述的場景和使用的方法,就又帶來了若干個問題。

開闢大量 chan 不進行關閉是否會導致記憶體洩漏?

記憶體是否洩露我們可以進行一下測試。觀察輸出結果很顯然沒有close也是可以被gc的。


// 無關閉無洩漏,會回收

func  goManyChannelTest(cmd *cobra.Command, args []string) {

var  m runtime.MemStats

runtime.ReadMemStats(&m)

fmt.Printf("%d B\n", m.Alloc/8)

var  i  int64

for  i = 0; i < 100000000000; i++ {

useChan()

if i%1000000 == 0 {

runtime.ReadMemStats(&m)

fmt.Printf("Thousand-hand:useMem %d KB\n", m.Alloc/1024/8)

}

}

}

func  useChan() {

cInt := make(chan  int, 0)

//defer close(cInt)

//fmt.Println(cInt)

if  false {

fmt.Println(cInt)

}

}

如何判斷一個 chan 是否關閉

下面只是其中一個方法,不是很推薦,僅僅是無資料情況下生效,並且在有資料的時候可能會導致資料被誤取,用 for .. range 也有同樣的問題


isClose := func(a chan  int) bool {

select {

case <-a:

return  true

default:

return  false

}

}

場景1問題:任務分發是否適合使用 channel

可以用,個人不是很推薦,可以去實現一個真實的佇列,如果可以接受定長佇列的話,也可以用 channel


var  mutex = &sync.Mutex{}

var  queueList = make(map[string][]string)

func  QueueRPush(key string, data ...string) {

mutex.Lock()

defer mutex.Unlock()

queue, _ := queueList[key]

queue = append(queue, data...)

queueList[key] = queue

}

func  QueueLPop(key string) (string, error) {

mutex.Lock()

defer mutex.Unlock()

queue, _ := queueList[key]

if  len(queue) > 0 {

result := queue[0]

queue = queue[1:]

queueList[key] = queue

return result, nil

}

return  "", errors.New("Queue is null")

}

func  QueueLen(key string) int {

mutex.Lock()

defer mutex.Unlock()

queue, ok := queueList[key]

if ok {

return  len(queue)

}

return  0

}

場景2問題:多工執行用 channel 判斷所有任務結束是否合適

可以用,也可以用 wg 去實現,個人認為兩者並無優劣


type  workJob  struct {

done chan  bool

}

// 可以為了實現功能而使用 channel ,不要為了使用 channel 而實現

func  channelManagerMoreGoFinish(cmd *cobra.Command, args []string) {

wList := []workJob{}

for  i := 1; i <= 10; i++ {

// 如果不初始化會阻塞

newJob := workJob{make(chan  bool)}

go  func(job workJob, i int) {

t := rand.Int31n(3)

fmt.Println("will sleep ", t, "second")

time.Sleep(time.Second * cast.ToDuration(t))

fmt.Println(i, "job will end")

job.done <- true

fmt.Println(i, "job end")

}(newJob, i)

wList = append(wList, newJob)

}

for  _, workJobItem := range wList {

<-workJobItem.done

}

fmt.Println("all end")

}

個人推薦使用 sync.WaitGroup 進行控制,可讀性更強。更容易理解


// Together 並行執行

func  Together(job func(goId int), counter int) {

var  wg sync.WaitGroup

for  i := 0; i <= counter; i++ {

wg.Add(1)

go  func(i int) {

defer wg.Done()

job(i)

}(i)

}

wg.Wait()

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

相關文章