Channel
channel概念
Channel 是Go中的一个核心类型,你可以把它看成一个管道。Channel是引用类型,操作符是箭头 <- 。
Channel 是 CSP 模式的具体实现,用于多个 goroutine 通讯。其内部实现了同步,确保并发安全。
Channel 是线程安全的,先进先出,多个goroutine同时访问,不需要加锁,channel是有类型的,一个整数的channel只能存放整数。
声明方式
//声明string类型的chan
var stringCh chan string
var mapCh chan map[int]string
//make定义 无缓冲channel
var ch1 chan int = make(chan int)
var ch2 = make(chan int)
ch3 := make(chan int)
//make定义 有缓冲的channel
var ch_buffer chan int = make(chan int, 10)
//只读channel
var ch_ro <-chan int
var ch_ro2 <-chan int = make(<-chan int, 10)
ch_ro3 := make(<-chan int, 10)
//只写channel
var ch_wo chan<- int
var ch2 chan<- int = make(chan<- int, 10)
var ch3 = make(chan<- int, 10)
ch4 := make(chan<- int, 10)
注意事项
缓冲区或者叫容量(capacity) 代表Channel容纳的最多的元素的数量,代表Channel的缓存大小。
如果没有设置容量,或者容量设置为0,说明Channel没有缓存,只有发送者(sender)和接受者(receiver)都准好了,它们的通讯(communication)才会发生Blocking(阻塞zu se)。如果设置了缓存,就有可能不发生阻塞,只有buffer满了之后发送者(sender)才会阻塞,而只有缓存空了后,receiver才会阻塞,一个nil channel 不会通信,往一个已经被close的channel中继续发送数据会导致run-time panic
往nil channel中发送数据会一直被阻塞着。
无缓冲的与有缓冲的channel有着重大差别,那就是无缓冲是同步的,有缓冲是非同步的。
-
无缓冲chan:ch1 := make(chan int)
-
有缓冲chan:ch2 := make(chan int, 1)
-
无缓冲 ch1<-1 不仅仅是向ch1通道写入1,而是要一直等待有别的协程接手这个参数,那么ch1<-1 才会继续下去,要不然就一直阻塞着。
-
有缓冲 ch2<-1 则不会阻塞,因为缓冲大小是1,只有当放第2个值的时候,第1个还没有被人拿走,这时候才会阻塞。
channel的操作
-
写channel
var ch = make(chan int,3) ch<-99 ch<-98 ch<-97
-
读channel
v,ok := <-ch if ok { fmt.Printf("读取chan:%v\n", v) }
-
关闭channel
close(ch) 注意:关闭之后再往里写数据会引发panic错误 `panic: send on closed channel`
完整示例
```Golang
func main() {
ch := make(chan int, 4) //声明
ch <- 99 //写入
ch <- 98
ch <- 97
ch <- 96
close(ch) //关闭
ch <- 95 //这里会引发panic
//以下是读取
for {
val, ok := <-ch
if ok == false { //false代表ch已经closed
fmt.Println("chan is closed")
break
}
fmt.Println(val)
}
}
channel的blocking
默认情况下,发送和接收回一直阻塞着,直到另一方准备好。这种方式可以用来在gororutine中进行同步,而不必使用显示的锁或者条件变量。
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c) // 728 17
go sum(s[len(s)/2:], c) //-940 -5
x,y:= <-c,<-c // receive from c 一直等到计算结果发送到channl中
fmt.Println(x,y,x+y) // 17 -5 12
}
channel的Range
package main
import (
"fmt"
"time"
)
func main() {
//开启一个goroutine
go func() {
time.Sleep(1 * time.Hour)
}()
//声明一个channel
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i //把i写入 channel
}
//close(c)
}()
//循环channel
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}
channel的select
- select的语句选择一组可能的
send
操作和receive
操作去处理。它类似switch
,但是只能用来处理通讯(communication)操作。它的case可以是send语句,也可以是receive语句,亦或是default
receive 语句可以将值赋值给一个或者两个变量。它必须是一个receive操作
package main
import "fmt"
func fibonacci(c, quit chan int) {
fmt.Println("3")
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
//验证执行的时间
fmt.Println("1")
go func() {
fmt.Println("4")
}()
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fmt.Println("2")
fibonacci(c, quit)
}
如果有同时多个case去处理,比如同时又多个channel可以接收数据,那么Go会随机选择一个case(pseudo-random 伪随机)处理。如果没有case需要处理,则会选择default去处理,如果没有default case,则select语句会阻塞,直到某个case需要处理
channel的timeout
select有很重要的一个应用就是超时处理。因为上面我们提到,如果没有case需要处理,select语句就会一直阻塞着。这时就需要一个超时处理
package main
import (
"fmt"
"time"
)
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}
结果输出timeout 1,它利用的是time.After方法
,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。
channel的Timer and Ticker
timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个Channel,在将来的那个时间那个Channel提供了一个时间值
timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")
用timer.Stop来停止计时器
package main
import (
"fmt"
"time"
)
func main() {
timer1 := time.NewTimer(time.Second * 2)
go func() {
<-timer1.C //这个C是一个time类型的只读channel
fmt.Println("Timer 1 is expired")
}()
stop2 := timer1.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}
}
ticker是一个定时触发的计时器,它会以一个间隔(interval)往Channel发送一个事件(当前时间),而Channel的接收者可以以固定的时间间隔从Channel中读取事件。
ticker := time.NewTicker(time.Millisecond * 500) //500ms
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
fmt.Println("啥都没有")
time.Sleep(time.Second * 6)
输出结果,可见是相差500ms
Tick at 2021-12-24 23:57:14.4941116 +0800 CST m=+0.506302301
Tick at 2021-12-24 23:57:14.9944728 +0800 CST m=+1.006663501
Tick at 2021-12-24 23:57:15.4945139 +0800 CST m=+1.506704601
Tick at 2021-12-24 23:57:15.9958106 +0800 CST m=+2.008001301
Tick at 2021-12-24 23:57:16.4957878 +0800 CST m=+2.507978501
Tick at 2021-12-24 23:57:16.9940308 +0800 CST m=+3.006221501
Tick at 2021-12-24 23:57:17.4953294 +0800 CST m=+3.507520101
Tick at 2021-12-24 23:57:17.9943069 +0800 CST m=+4.006497601
Tick at 2021-12-24 23:57:18.4940752 +0800 CST m=+4.506265901
Tick at 2021-12-24 23:57:18.9949129 +0800 CST m=+5.007103601
Tick at 2021-12-24 23:57:19.4946122 +0800 CST m=+5.506802901
channel 同步
channel可以用在goroutine之间的同步
package main
import (
"fmt"
"time"
)
func worker(done chan bool) {
time.Sleep(time.Second)
done <- true
}
func main() {
done := make(chan bool, 1)
go worker(done)
select {
case <-done:
fmt.Println("oh finished")
}
}
main goroutine通过done channel等待worker完成任务。 worker做完任务后只需往channel发送一个数据就可以通知main goroutine任务完成。