go channel學習使用

 

出處:https://www.cnblogs.com/jiujuan/p/16014608.html

什麼是 channel 管道#

它是一個數據管道,可以往裏面寫數據,從裏面讀數據。

channel 是 goroutine 之間數據通信橋樑,而且是線程安全的。

channel 遵循先進先出原則。

寫入,讀出數據都會加鎖。

channel 可以分爲 3 種類型:

  • 只讀 channel,單向 channel

  • 只寫 channel,單向 channel

  • 可讀可寫 channel

channel 還可按是否帶有緩衝區分爲:

  • 帶緩衝區的 channel,定義了緩衝區大小,可以存儲多個數據

  • 不帶緩衝區的 channel,只能存一個數據,並且只有當該數據被取出才能存下一個數據

channel 的基本使用#

定義和聲明#

Copy
// 只讀 channel
var readOnlyChan <-chan int  // channel 的類型爲 int

// 只寫 channel
var writeOnlyChan chan<- int

// 可讀可寫
var ch chan int

// 或者使用 make 直接初始化
readOnlyChan1 := make(<-chan int, 2)  // 只讀且帶緩存區的 channel
readOnlyChan2 := make(<-chan int)   // 只讀且不帶緩存區 channel

writeOnlyChan3 := make(chan<- int, 4) // 只寫且帶緩存區 channel
writeOnlyChan4 := make(chan<- int) // 只寫且不帶緩存區 channel

ch := make(chan int, 10)  // 可讀可寫且帶緩存區

ch <- 20  // 寫數據
i := <-ch  // 讀數據
i, ok := <-ch  // 還可以判斷讀取的數據

chan_var.go

Copy

package main

import (
    "fmt"
)

func main() {
    // var 聲明一個 channel,它的零值是nil
    var ch chan int
    fmt.Printf("var: the type of ch is %T \n", ch)
    fmt.Printf("var: the val of ch is %v \n", ch)

    if ch == nil {
        // 也可以用make聲明一個channel,它返回的值是一個內存地址
        ch = make(chan int)
        fmt.Printf("make: the type of ch is %T \n", ch)
        fmt.Printf("make: the val of ch is %v \n", ch)
    }

    ch2 := make(chan string, 10)
    fmt.Printf("make: the type of ch2 is %T \n", ch2)
    fmt.Printf("make: the val of ch2 is %v \n", ch2)
}

// 輸出:
// var: the type of ch is chan int
// var: the val of ch is <nil>
// make: the type of ch is chan int
// make: the val of ch is 0xc000048060
// make: the type of ch2 is chan string
// make: the val of ch2 is 0xc000044060

操作channel的3種方式#

操作 channel 一般有如下三種方式:

  1. 讀 <-ch

  2. 寫 ch<-

  3. 關閉 close(ch)

操作nil的channel正常channel已關閉的channel
讀 <-ch 阻塞 成功或阻塞 讀到零值
寫 ch<- 阻塞 成功或阻塞 panic
關閉 close(ch) panic 成功 panic

注意 對於 nil channel 的情況,有1個特殊場景:

當 nil channel 在 select 的某個 case 中時,這個 case 會阻塞,但不會造成死鎖。

單向 channel#

單向 channel:只讀和只寫的 channel

chan_uni.go

Copy
package main

import "fmt"

func main() {
	// 單向 channel,只寫channel
	ch := make(chan<- int)
	go testData(ch)
	fmt.Println(<-ch)
}

func testData(ch chan<- int) {
	ch <- 10
}

// 運行輸出
// ./chan_uni.go:9:14: invalid operation: <-ch (receive from send-only type chan<- int)
// 報錯,它是一個只寫 send-only channel

把上面代碼main()函數裏初始化的單向channel,修改爲可讀可寫channel,再運行

chan_uni2.go

Copy
package main

import "fmt"

func main() {
    // 把上面代碼main()函數初始化的單向 channel 修改爲可讀可寫的 channel
	ch := make(chan int)
	go testData(ch)
	fmt.Println(<-ch)
}

func testData(ch chan<- int) {
	ch <- 10
}

// 運行輸出:
// 10

// 沒有報錯,可以正常輸出結果

帶緩衝和不帶緩衝的 channel#

不帶緩衝區 channel#

chan_unbuffer.go

Copy
package main

import "fmt"

func main() {
    ch := make(chan int) // 無緩衝的channel
    go unbufferChan(ch)

    for i := 0; i < 10; i++ {
        fmt.Println("receive ", <-ch) // 讀出值
    }
}

func unbufferChan(ch chan int) {
    for i := 0; i < 10; i++ {
        fmt.Println("send ", i)
        ch <- i // 寫入值
    }
}

// 輸出
send  0
send  1
receive  0
receive  1
send  2
send  3
receive  2
receive  3
send  4
send  5
receive  4
receive  5
send  6
send  7
receive  6
receive  7
send  8
send  9
receive  8
receive  9

帶緩衝區 channel#

chan_buffer.go

Copy
package main

import (
	"fmt"
)

func main() {
	ch := make(chan string, 3)
	ch <- "tom"
	ch <- "jimmy"
	ch <- "cate"

	fmt.Println(<-ch)
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

// 運行輸出:
// tom
// jimmy
// cate

再看一個例子:chan_buffer2.go

Copy
package main

import (
	"fmt"
	"time"
)

var c = make(chan int, 5)

func main() {
	go worker(1)
	for i := 1; i < 10; i++ {
		c <- i
		fmt.Println(i)
	}
}

func worker(id int) {
	for {
		_ = <-c
	}
}

// 運行輸出:
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

判斷 channel 是否關閉#

語法:

Copy
v, ok := <-ch

說明:

  • ok 爲 true,讀到數據,管道沒有關閉
  • ok 爲 false,管道已關閉,沒有數據可讀
Copy
// 代碼片段例子
if v, ok := <-ch; ok {
    fmt.Println(v)
}

舉一個完整例子:

Copy
package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)
	go test(ch)

	for {
		if v, ok := <-ch; ok {
			fmt.Println("get val: ", v, ok)
		} else {
			break
		}

	}
}

func test(ch chan int) {
	for i := 0; i < 5; i++ {
		ch <- i
	}
	close(ch)
}

讀已經關閉的 channel 會讀到零值,如果不確定 channel 是否關閉,可以用這種方法來檢測。

range and close#

range 可以遍歷數組,map,字符串,channel等。

一個發送者可以關閉 channel,表明沒有任何數據發送給這個 channel 了。接收者也可以測試channel是否關閉,通過 v, ok := <-ch 表達式中的 ok 值來判斷 channel 是否關閉。上一節已經說明 ok 爲 false 時,表示 channel 沒有接收任何數據,它已經關閉了。

注意:僅僅只能是發送者關閉一個 channel,而不能是接收者。給已經關閉的 channel 發送數據會導致 panic。

Note: channels 不是文件,你通常不需要關閉他們。那什麼時候需要關閉?當要告訴接收者沒有值發送給 channel 了,這時就需要了。

比如終止 range 循環。

當 for range 遍歷 channel 時,如果發送者沒有關閉 channel 或在 range 之後關閉,都會導致 deadlock(死鎖)。

下面是一個會產生死鎖的例子:

Copy
package main

import "fmt"

func main() {
	ch := make(chan int)

	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
	}()

	for val := range ch {
		fmt.Println(val)
	}
	close(ch) // 這裏關閉channel已經”通知“不到range了,會觸發死鎖。
              // 不管這裏是否關閉channel,都會報死鎖,close(ch)的位置就不對。
              // 且關閉channel的操作者也錯了,只能是發送者關閉channel
}
// 運行程序輸出
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// fatal error: all goroutines are asleep - deadlock!

改正也很簡單,把 close(ch) 移到 go func(){}() 裏,代碼如下

Copy
go func() {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
}()

這樣程序就可以正常運行,不會報 deadlock 的錯誤了。

把上面程序換一種方式來寫,chan_range.go

Copy
package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)
	go test(ch)
	for val := range ch { //
		fmt.Println("get val: ", val)
	}
}

func test(ch chan int) {
	for i := 0; i < 5; i++ {
		ch <- i
	}
	close(ch)
}

// 運行輸出:
// get val:  0
// get val:  1
// get val:  2
// get val:  3
// get val:  4

發送者關閉 channel 時,for range 循環自動退出。

for 讀取channel#

用 for 來不停循環讀取 channel 裏的數據。

把上面的 range 程序修改下,chan_for.go

Copy
package main

import (
	"fmt"
)

func main() {
	ch := make(chan int)
	go test(ch)

	for {
		val, ok := <-ch
		if ok == false {// ok 爲 false,沒有數據可讀
			break // 跳出循環
		}
		fmt.Println("get val: ", val)
	}
}

func test(ch chan int) {
	for i := 0; i < 5; i++ {
		ch <- i
	}
	close(ch)
}

// 運行輸出:
// get val:  0
// get val:  1
// get val:  2
// get val:  3
// get val:  4

select 使用#

例子 chan_select.go

Copy
package main

import "fmt"

// https://go.dev/tour/concurrency/5
func fibonacci(ch, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case ch <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	ch := make(chan int)
	quit := make(chan int)

	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-ch)
		}
		quit <- 0
	}()

	fibonacci(ch, quit)
}

// 運行輸出:
// 0
// 1
// 1
// 2
// 3
// 5
// 8
// 13
// 21
// 34
// quit

channel 的一些使用場景#

1. 作爲goroutine的數據傳輸管道#

Copy
package main

import "fmt"

// https://go.dev/tour/concurrency/2
func sums(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sums(s[:len(s)/2], c)
	go sums(s[len(s)/2:], c)

	x, y := <-c, <-c // receive from c

	fmt.Println(x, y, x+y)
}

用 goroutine 和 channel 分批求和

2. 同步的channel#

沒有緩衝區的 channel 可以作爲同步數據的管道,起到同步數據的作用。

對沒有緩衝區的 channel 操作時,發送的 goroutine 和接收的 goroutine 需要同時準備好,也就是發送和接收需要一一配對,才能完成發送和接收的操作。

如果兩方的 goroutine 沒有同時準備好,channel 會導致先執行發送或接收的 goroutine 阻塞等待。這就是沒有緩衝區的 channel 作爲數據同步的作用。

gobyexample 中的一個例子:

Copy
package main

import (
	"fmt"
	"time"
)

//https://gobyexample.com/channel-synchronization
func worker(done chan bool) {
	fmt.Println("working...")
	time.Sleep(time.Second)
	fmt.Println("done")

	done <- true
}

func main() {
	done := make(chan bool, 1)
	go worker(done)

	<-done
}

注意:同步的 channel 千萬不要在同一個 goroutine 協程裏發送和接收數據。可能導致deadlock死鎖。

3. 異步的channel#

有緩衝區的 channel 可以作爲異步的 channel 使用。

有緩衝區的 channel 也有操作注意事項:

  1. 如果 channel 中沒有值了,channel 爲空了,那麼接收者會被阻塞。

  2. 如果 channel 中的緩衝區滿了,那麼發送者會被阻塞。

    注意:有緩衝區的 channel,用完了要 close,不然處理這個channel 的 goroutine 會被阻塞,形成死鎖。

Copy
package main

import (
	"fmt"
)

func main() {
	ch := make(chan int, 4)
	quitChan := make(chan bool)

	go func() {
		for v := range ch {
			fmt.Println(v)
		}
		quitChan <- true // 通知用的channel,表示這裏的程序已經執行完了
	}()

	ch <- 1
	ch <- 2
	ch <- 3
	ch <- 4
	ch <- 5

	close(ch)  // 用完關閉channel
	<-quitChan // 接到channel通知後解除阻塞,這也是channel的一種用法
}

4.channel 超時處理#

channel 結合 time 實現超時處理。

當一個 channel 讀取數據超過一定時間還沒有數據到來時,可以得到超時通知,防止一直阻塞當前 goroutine。

chan_timeout.go

Copy
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)
	quitChan := make(chan bool)

	go func() {
		for {
			select {
			case v := <-ch:
				fmt.Println(v)
			case <-time.After(time.Second * time.Duration(3)):
				quitChan <- true
				fmt.Println("timeout, send notice")
				return
			}
		}
	}()

	for i := 0; i < 4; i++ {
		ch <- i
	}

	<-quitChan // 輸出值,相當於收到通知,解除主程阻塞
	fmt.Println("main quit out")
}

使用 channel 的注意事項及死鎖分析#

未初始化的 channel 讀寫關閉操作#

1.讀:未初始化的channel,讀取裏面的數據時,會造成死鎖deadlock

Copy
var ch chan int
<-ch  // 未初始化channel讀數據會死鎖

2.寫:未初始化的channel,往裏面寫數據時,會造成死鎖deadlock

Copy
var ch chan int
ch<-  // 未初始化channel寫數據會死鎖

3.關閉:未初始化的channel,關閉該channel時,會panic

Copy
var ch chan int
close(ch) // 關閉未初始化channel,觸發panic

已初始化的 channel 讀寫關閉操作#

1. 已初始化,沒有緩衝區的channel#

Copy
   // 代碼片段1
   func main() {
        ch := make(chan int)
        ch <- 4
   }

代碼片段1:沒有緩衝channel,且只有寫入沒有讀取,會產生死鎖


Copy
   // 代碼片段2
   func main() {
       ch := make(chan int)
       val, ok := <-ch
   }

代碼片段2:沒有緩衝channel,且只有讀取沒有寫入,會產生死鎖


Copy
   // 代碼片段3
   func main() {
       ch := make(chan int)
       val, ok := <-ch
       if ok {
           fmt.Println(val)
       }
       ch <- 10 // 這裏進行寫入。但是前面已經產生死鎖了
   }

代碼片段3:沒有緩衝channel,既有寫入也有讀出,但是在代碼 val, ok := <-c 處已經產生死鎖了。下面代碼執行不到。


Copy
   // 代碼片段4
   func main() {
   	ch := make(chan int)
   	ch <- 10
   	go readChan(ch)
   	
       time.Sleep(time.Second * 2)
   }
   
   func readChan(ch chan int) {
   	for {
   		val, ok := <-ch
   		fmt.Println("read ch: ", val)
   		if !ok {
   			break
   		}
   	}
   }

代碼片段4:沒有緩衝channel,既有寫入也有讀出,但是運行程序後,報錯 fatal error: all goroutines are asleep - deadlock! 。

這是因爲往 channle 裏寫入數據的代碼 ch <- 10,這裏寫入數據時就已經產生死鎖了。把 ch<-10 和 go readChan(ch) 調換位置,程序就能正常運行,不會產生死鎖。


Copy
   // 代碼片段5
   func main() {
   	ch := make(chan int)
   
   	go writeChan(ch)
   
   	for {
   		val, ok := <-ch
   		fmt.Println("read ch: ", val)
   		if !ok {
   			break
   		}
   	}
   
   	time.Sleep(time.Second)
       fmt.Println("end")
   }
   
   func writeChan(ch chan int) {
   	for i := 0; i < 4; i++ {
   		ch <- i
   	}
   }

代碼片段5:沒有緩衝的channel,既有寫入,也有讀出,與上面幾個代碼片段不同的是,寫入channel的數據不是一個。

思考一下,這個程序會產生死鎖嗎?10 秒時間思考下,先不要看下面。



也會產生死鎖,它會輸出完數據後,報錯 fatal error: all goroutines are asleep - deadlock!

爲什麼呢?這個程序片段,既有讀也有寫而且先開一個goroutine寫數據,爲什麼會死鎖?

原因在於 main() 裏的 for 循環。可能你會問,不是有 break 跳出 for 循環嗎?代碼是寫了,但是程序並沒有執行到這裏。

因爲 for 會不停的循環,而 val, ok := <-ch, 這裏 ok 值一直是 true,因爲程序裏並沒有哪裏關閉 channel 啊。你們可以打印這個 ok 值看一看是不是一直是 true。當 for 循環把 channel 裏的值讀取完了後,程序再次運行到 val, ok := <-ch 時,產生死鎖,因爲 channel 裏沒有數據了。

找到原因了,那解決辦法也很簡單,在 writeChan 函數裏關閉 channel,加上代碼 close(ch)。告訴 for 我寫完了,關閉 channel 了。

加上關閉 channel 代碼後運行程序:

Copy
read ch:  0 , ok:  true
read ch:  1 , ok:  true
read ch:  2 , ok:  true
read ch:  3 , ok:  true
read ch:  0 , ok:  false
end

程序正常輸出結果。

對於沒有緩衝區的 channel (unbuffered channel) 容易產生死鎖的幾個代碼片段分析,總結下:

  1. channel 要用 make 進行初始化操作
  2. 讀取和寫入要配對出現,並且不能在同一個 goroutine 裏
  3. 一定先用 go 起一個協程執行讀取或寫入操作
  4. 多次寫入數據,for 讀取數據時,寫入者注意關閉 channel(代碼片段5)

2. 已初始化,有緩衝區的 channel#

Copy
// 代碼片段1
func main() {
    ch := make(chan int, 1)
    val, ok := <-ch
}

代碼片段1:有緩衝channel,先讀數據,這裏會一直阻塞,產生死鎖。


Copy
   // 代碼片段2
   func main() {
       ch := make(chan int, 1)
       ch <- 10
       ch <- 10
   }

代碼片段2:同代碼片段1,有緩衝channel,且 channel 緩衝只有容量 1(片段2示例),寫多個值而沒有讀,也會阻塞,產生死鎖。

Copy
   // 代碼片段3
   func main() {
   	ch := make(chan int, 1)
   	ch <- 10
   
   	val, ok := <-ch
   	if ok {
   		fmt.Println(val, ok)
   	}
   }

代碼片段3:有緩衝的channel,有讀有寫,正常的輸出結果。


有緩衝區的channel總結:

  1. 如果 channel 滿了,發送者會阻塞
  2. 如果 channle 空了,接收者會阻塞
  3. 如果在同一個 goroutine 裏,寫數據操作一定在讀數據操作前

 

 

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