今天看到一篇有關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 協議》,轉載必須註明作者和本文連結