第八章(并发)[二]
通道
-
Go鼓励使用CSP(Communicating Sequential Process)
-
作为CSP核心,Channel是显式的,要求操作双方必须知道数据类型和具体通道,并不关心另一端操作者的身份和数量。如果另一端未准备妥当,或消息未能及时处理时,会阻塞当前端
-
从底层实现上来说,通道只是一个队列。
-
同步模式下,发送和接收双方配对,然后直接复制数据给对方。如配对失败,则置入等待队列,直到另一方出现后被唤醒。
-
异步模式下,抢夺的是数据缓冲槽。发送方要求有空槽可供写入,而接收方则要求有缓冲数据可读。 需求不符时,同样加入等待队列,直到另一方写入数据或腾出空槽后被唤醒
-
除传递消息外(数据)外,通道还常被用做事件通知。
func main() {
done := make(chan struct{})
c := make(chan string)
go func() {
s := <-c //接收消息
println(s)
close(done) //关闭通道,作为结束通知
}()
c <- "Hi" //发送消息
<-done //阻塞,直到有数据或关闭
}
同步模式必须有配对操作的
goroutine
,否则会一直阻塞
- 异步模式在缓冲区未满或数据未读完前,不会阻塞
func main() {
c := make(chan int, 3)
c <- 1
c <- 2
println(<-c)
println(<-c)
}
多数时候,异步通道有助于提升性能,减少排队阻塞
-
缓冲区大小是通道的内部属性,不属于类型的组成部分。
-
通道变量本身就是指针,可用相等操作符判断是否为同一对象或nil
func main() {
var a, b = make(chan int, 3), make(chan int)
var c chan bool
println(a == b) //false
println(c == nil) //true
fmt.Printf("%v,%T\n", a, a) //0xc0000d4080,chan int
fmt.Printf("%v,%T\n", b, b) //0xc000086060,chan int
fmt.Printf("%p,%d\n", a, unsafe.Sizeof(a)) //0xc0000d4080,8
fmt.Printf("%p,%d\n", b, unsafe.Sizeof(b)) //0xc000086060,8
}
-
内部函数
cap
和len
返回缓冲区大小和当前已缓冲数量 -
对于同步通道则返回0。据此可判断通道是同步还是异步
-
除使用简单的发送或接收操作符外,还可用ok-idom或range模式处理数据
func main() {
done := make(chan struct{})
c := make(chan int)
go func() {
defer close(done)
for {
x, ok := <-c //判断通道是否关闭
if !ok {
return
}
println(x)
}
}()
c <- 1
c <- 2
c <- 3
close(c)
<-done
}
使用
ok-idom
判断通道是否关闭
- 通知可以是群体性的,
func main() {
var wg sync.WaitGroup
ready := make(chan struct{})
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
println(id, ": ready.")
<-ready
println(id, ": running...")
}(i)
}
time.Sleep(time.Second)
println("Ready? Go!")
close(ready)
wg.Wait()
}
-
对于closed或nil通道,发送和接收操作都有相应规则:
- 向已关闭通道发送数据,引发panic
- 从已关闭通道接收数据,返回已缓冲数据或零值
- 无论收发,nil通道都会阻塞
func main() {
c := make(chan int, 3)
c <- 10
c <- 20
close(c)
for i := 0; i < cap(c); i++ {
x, ok := <-c
println(i, ":", ok, x)
}
}
内建函数close关闭信道,该通道必须为双向的或只发送的。它应当只由发送者执行,而不应由接收者执行,其效果是在最后发送的值被接收后停止该通道。在最后的值从已关闭的信道中被接收后,任何对其的接收操作都会无阻塞的成功。
- 重复关闭,或关闭nil通道都会引发panic错误(panic: close of closed channel | close of nil channel)
单向通道
-
通道默认是双向的,并不区分发送端和接收端
-
尽管可用make创建单向通道,但那没有意义。
-
通常使用类型转换来获取单向通道,并分别赋予操作双方
func main() {
var wg sync.WaitGroup
wg.Add(2)
c := make(chan int)
var send chan<- int = c //只写通道
var recv <-chan int = c //只读通道
go func() {
defer wg.Done()
for x := range recv {
println(x)
}
}()
go func() {
defer wg.Done()
defer close(c)
for i := 0; i < 3; i++ {
send <- i
}
}()
wg.Wait()
}
- 不能在单向通道上做逆向操作
func main() {
c := make(chan int, 2)
var send chan<- int = c
var recv <-chan int = c
<-send //invalid operation: <-send (receive from send-only type chan<- int)
recv <- 1 //invalid operation: recv <- 1 (send to receive-only type <-chan int)
}
- close不能用于接收端(还记得上面案例中:当close通道后,当接收端读取数据完毕后才真正关闭通道)
func main() {
c := make(chan int, 2)
var recv <-chan int = c
close(recv) //invalid operation: close(recv) (cannot close receive-only channel)
}
- 无法将单向通道转换双向通道
select
- 如果同时处理多个通道,可选用select语句(它会随机选择一个可用通道做收发操作)
func main() {
var wg sync.WaitGroup
wg.Add(2)
a, b := make(chan int), make(chan int)
go func() {
defer wg.Done()
for {
var (
name string
x int
ok bool
)
select {
case x, ok = <-a:
name = "a"
case x, ok = <-b:
name = "b"
}
if !ok { //任何通道关闭,则终止接收
return
}
println(name, x) //输出接收的数据信息
}
}()
go func() {
defer wg.Done()
defer close(a)
defer close(b)
for i := 0; i < 10; i++ {
select { //随机选择发送channel
case a <- i:
case b <- i * 10:
}
}
}()
wg.Wait()
}
- 如果要等全部通道消息处理结束,可将已完成通道设置为nil。这样它就会被阻塞并不再被select选中
func main() {
var wg sync.WaitGroup
wg.Add(3)
a, b := make(chan int), make(chan int)
go func() {
defer wg.Done()
for {
select {
case x, ok := <-a:
if !ok {
a = nil
break
}
println("a", x) //输出接收的数据信息
case x, ok := <-b:
if !ok {
b = nil
break
}
println("b", x)
}
if nil == a && nil == b {
return
}
}
}()
go func() {
defer wg.Done()
defer close(a)
for i := 0; i < 3; i++ {
a <- i
}
}()
go func() {
defer wg.Done()
defer close(b)
for i := 0; i < 5; i++ {
b <- i * 10
}
}()
wg.Wait()
/*
a 0
a 1
a 2
b 0
b 10
b 20
b 30
b 40
*/
}
-
即便是同一通道,也会随机选择case执行
-
当所有通道都不可用时,select会执行default语句。(可避开select阻塞)