channel 通道
原子函数和互斥锁都能工作,但是依靠它们都不会让编写并发程序变得更简单,更不容易出错,或者更有趣。在 Go 语言里,你不仅可以使用原子函数和互斥锁来保证对共享资源的安全访问以及消除竞争状态,还可以使用通道,通过发送和接收需要共享的资源,在goroutine之间做同步。
当一个资源需要在 goroutine 之间共享时,通道在 goroutine 之间架起了一个管道,并提供了 确保同步交换数据的机制。声明通道时,需要指定将要被共享的数据的类型。可以通过通道共享 内置类型、命名类型、结构类型和引用类型的值或者指针。
// 无缓冲的整型通道
unbuffered := make(chan int)
// 有缓冲的字符串通道
buffered := make(chan string, 10)
无缓冲的通道
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通 道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个 goroutine 没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。这种对通道进行发送 和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在
(此图来自书籍《Go语言实战》)
上代码示例:
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
var wg sync.WaitGroup
func init() {
rand.Seed(time.Now().UnixMicro())
}
func main() {
wg.Add(2)
ball := 1
court := make(chan int)
go palyer("汤姆", court)
go palyer("杰瑞", court)
court <- ball
wg.Wait()
}
func palyer(name string, court chan int) {
defer wg.Done()
for {
ball, ok := <-court
if !ok {
fmt.Printf("%s赢得了比赛!\n", name)
return
}
n := rand.Intn(100)
if n%3 == 0 {
fmt.Printf("%s失球!\n", name)
close(court)
return
}
fmt.Printf("%s成功击球!\n", name)
court <- ball
}
}
程序运行结果
杰瑞成功击球!
汤姆成功击球!
杰瑞成功击球!
汤姆失球!
杰瑞赢得了比赛!
疑问
court <- ball //位置调到开启goroutine之前
go palyer("汤姆", court)
go palyer("杰瑞", court)
// 程序能正常执行吗?
很遗憾,程序无法正常执行
fatal error: all goroutines are asleep - deadlock!
编译器提示所有goroutines都睡着了,死锁!
原因是无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通 道要求发送 goroutine 和接收 goroutine 同时准备好
举个例子:汤姆和杰瑞都没入场,球就已经发出但却先被泰菲偷偷拿走了,导致后面两人进场时都在等球,但球却一直没有发出
有缓存区的通道
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。
通道会阻塞发送和接收动作的条件也会不同。 只有在通道中没有要接收的值时,接收动作才会阻塞。 只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
这导致有缓冲的通道和无缓冲的通道之间的一个很大 的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的 通道没有这种保证
(此图来自书籍《Go语言实战》)
既然有缓存区的通道被接收前能存储一个或者多个值的通道,那我上面的案例就可以解决代码会产生死锁的问题
court := make(chan int, 1) //有缓冲区的通道
court <- ball
go palyer("汤姆", court)
go palyer("杰瑞", court)
wg.Wait()
程序可正常执行
这里设置缓存大小为 1,表示这个通道最多只能容纳一个球。这样在某个选手击球后,即使另一个选手此时正在获取球,也不会发生阻塞,因为通道中已经有一个球了。
close()
close() 是一个内置函数,用于关闭一个通道(channel)。关闭通道后,无法再向其中发送数据,但仍然可以从中读取已有的数据。在通道被关闭后,若继续向其发送数据,则会导致 panic。
能够从已经关闭的通道接收数据这一点非常重要,因为这允许通道关闭后依旧能取出其中缓冲的全部值,而不会有数据丢失。从一个已经关闭且没有数据的通道里获取数据,总会立刻返回,并返回一个通道类型的零值。如果在获取通道时还加入了可选的标志,就能得到通道的状态信息
task, ok := <-tasks
if !ok{
//...
}
返回值是一个bool类型,通过他可以对通道内部情况进行判断
总结
- 通道提供了一种在两个 goroutine 之间共享数据的简单方法。
- 无缓冲的通道保证同时交换数据,而有缓冲的通道不做这种保证。
参考书籍《Go语言实战》