golang 面試題(十二)chan緩存池

1.下面的迭代會有什麼問題?

func (set *threadSafeSet) Iter() <-chan interface{} {
    ch := make(chan interface{})
    go func() {
        set.RLock()

        for elem := range set.s {
            ch <- elem
        }

        close(ch)
        set.RUnlock()

    }()
    return ch
}

考點:chan緩存池
解答:
看到這道題,我也在猜想出題者的意圖在哪裏。 chan?sync.RWMutex?go?chan緩存池?迭代? 所以只能再讀一次題目,就從迭代入手看看。 既然是迭代就會要求set.s全部可以遍歷一次。但是chan是爲緩存的,那就代表這寫入一次就會阻塞。 我們把代碼恢復爲可以運行的方式,看看效果

package main

import (
	"fmt"
	"sync"
)

//下面的迭代會有什麼問題?

type threadSafeSet struct {
	sync.RWMutex
	s []interface{}
}

func (set *threadSafeSet) Iter() <-chan interface{} {
	ch := make(chan interface{}) // 解除註釋看看!
	//ch := make(chan interface{},len(set.s))
	go func() {
		set.RLock()

		for elem,value := range set.s {
			ch <- elem
			println("Iter:",elem,value)
		}

		close(ch)
		set.RUnlock()

	}()
	return ch
}

func main()  {

	th:=threadSafeSet{
		s:[]interface{}{"1","2"},
	}
	vs := th.Iter()
	fmt.Println(<- vs)
}

設置緩衝區輸出:

或:

不設置緩衝區輸出:

如果不設置緩衝區,寫入chan的代碼 ch <- elem 將只執行一次就會阻塞。

 

-------------------------將代碼改爲循環讀取,不設置緩衝區:-------------------------

package main

import (
	"fmt"
	"sync"
)

//下面的迭代會有什麼問題?

type threadSafeSet struct {
	sync.RWMutex
	s []interface{}
}

func (set *threadSafeSet) Iter() <-chan interface{} {
	ch := make(chan interface{}) // 解除註釋看看!
	//ch := make(chan interface{},len(set.s))
	go func() {
		set.RLock()

		for elem,value := range set.s {
			ch <- elem
			println("Iter:",elem,value)
		}

		close(ch)
		set.RUnlock()

	}()
	return ch
}

func main()  {

	th:=threadSafeSet{
		s:[]interface{}{"1","2"},
	}
	vs := th.Iter()
	for true {
		v,ok :=  <- vs
		if ok {
			fmt.Println(v)
		}else {
			return
		}
	}

	//fmt.Println(<- vs)
}

結果:

改爲不設置緩衝區循環讀取後寫入chan的代碼 ch <- elem 也執行了兩次,但是第二次寫入操作一定是在第一次讀取之後。

如果設置緩衝區,則寫入順序和讀取順序不固定。

 

 

-------------------------接下來將close(ch)這行代碼註釋掉。-------------------------

程序運行結果:

無論是否設置緩衝區都會報fatal error: all goroutines are asleep - deadlock!的錯。

 

-------------------------再將循環讀取改爲讀取一次。-------------------------

func main()  {
	th:=threadSafeSet{
		s:[]interface{}{"1","2"},
	}
	vs := th.Iter()

	time.Sleep(2*time.Second)
	//for true {
	//	v,ok :=  <- vs
	//	if ok {
	//		fmt.Println(v)
	//	}else {
	//		return
	//	}
	//}
	fmt.Println(<- vs)
}

結果:程序不再報錯,如圖:

 

將循環讀取操作另開一個線程:

func main()  {

	th:=threadSafeSet{
		s:[]interface{}{"1","2"},
	}
	vs := th.Iter()
	go func() {
		for true {
			v,ok :=  <- vs
			if ok {
				fmt.Println(v)
			}else {
				return
			}
		}
	}()
	
	fmt.Println(<- vs)
}

執行結果:

確實不再報錯,但事實並不是這樣:

接下來修改代碼:

func main()  {

	th:=threadSafeSet{
		s:[]interface{}{"1","2"},
	}
	vs := th.Iter()
	go func() {
		for true {
			v,ok :=  <- vs
			if ok {
				fmt.Println(v)
			}else {
				return
			}
		}
	}()

	go func() {
		//一直阻塞
		select {}
	}()

	fmt.Println(<- vs)
}

執行結果:

我們在代碼中增加了新的線程,該線程一直阻塞,但是並沒有報死鎖。

所以看起來這種方法可以解決報錯,是因爲新建的go線程中的錯誤並沒有打印。

@歡迎指正。

 

真正的原因:

分析問題應該是出在用FOR … RANGE 這種方法讀取CHAN這一塊,因爲實際上START到底有多少系統並不知道,所以一直在讀取CHAN,而CHAN一直沒有數據,就阻塞了(這裏並沒有顯式的close掉chan)。而用上面註釋代碼指定次數的方法就避免了這種阻塞,所以這裏會出錯,不知道這樣理解對不對?

對於這種次數不確定的goroutine,解決辦法是for … select 這種方法?

 

準確地說,是提示”deadlock”。
當worker都執行完,所在的goroutine都結束以後。只剩下main所在的goroutine在讀取start數據,但是這時候沒有其他的goroutine給start寫入數據,所以main就會一直在等待一個不可能出現的數據,結果就死鎖了。

 

有兩種方法解決這個問題:
1.知道channel要收到多少數據,當讀取到第10個數據以後就不再讀取。
2.當不需要再寫入數據時關閉channel,讀取channel的數據獲知關閉狀態則退出循環。

 

參考鏈接:https://www.golangtc.com/t/557c1b33b09ecc2aa700008f

 

驗證:

func main()  {

	th:=threadSafeSet{
		s:[]interface{}{"1","2"},
	}
	vs := th.Iter()
	i := 1
	for true {
		fmt.Printf("第%v次讀取\n",i)
		v,ok :=  <- vs
		if ok {
			fmt.Println(v)
		}else {
			return
		}
		i ++
	}

	fmt.Println(<- vs)
}

程序第三次讀取時會報死鎖,也解釋了上文單次讀取不報錯的原因。

 

參考鏈接:https://blog.csdn.net/u011328417/article/details/89473323

go testDeadLock(c) c<- 'A' 只要換下位置就不會報錯了,無緩存通道要先接受,後發送。

 

據瞭解,chan的緩衝區要麼設置0要麼設置1

超過1都不建議使用,不如用專業的消息隊列中間件。

緩衝爲1,實現異步協程,爲0實現同步。

 

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