开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 8 天,点击查看活动详情
channel
channel是go语言基于CSP模型创建的gorouting间的通讯机制。它可以实现:
- 数据交流:当作并发的 buffer 或者 queue,解决生产者 - 消费者问题。多个 goroutine 可以并发当作生产者(Producer)和消费者(Consumer)。
- 数据传递:一个 goroutine 将数据交给另一个 goroutine,相当于把数据的拥有权 (引用) 托付出去。
- 信号通知:一个 goroutine 可以将信号 (closing、closed、data ready 等) 传递给另一个或者另一组 goroutine 。
- 任务编排:可以让一组 goroutine 按照一定的顺序并发或者串行的执行,这就是编排的功能。
- 锁:利用 Channel 也可以实现互斥锁的机制。
CSP
CSP(Communicating Sequential Processes)通信顺序进程
不要以共享内存的方式来通信,相反,要通过通信来共享内存
go语言实现主要包含channel和gorouting,channel实现gorouting间的通信
channel的基本使用
channel和Slice、map一样需要make开炮空间才能使用。
var ch1 chan int
ch1 = make(chan int)
ch2 = make(chan int,5)
go func() {
ch1 <- 3 // 向channel发送数据
}()
fmt.Println(<-ch1) // 接收数据
close(ch1) //关闭
channel种类:
- 无缓冲channel:没有缓冲区,收发者同时在场才能通信
- 有缓冲channel:有一定容量的缓冲区,更加灵活自由,不会直接阻塞
- send-only:只发送
- recv-only:只接收
只发送 channel 类型和只接收 channel 类型,会被用作函数的参数类型或返回值,用于限制对 channel 内的操作,或者是明确可对 channel 进行的操作的类型
func produce(ch chan<- int) {
for i := 0; i < 10; i++ {
ch <- i + 1
time.Sleep(time.Second)
}
close(ch)
}
func consume(ch <-chan int) {
for n := range ch {
println(n)
}
}
ch := make(chan int, 5)
go produce(ch)
go consume(ch)
channel的数据接收与关闭:
- 通常channel在发送方进行关闭
- n := <-ch1,当ch被关闭后,n将被赋值为ch元素类型的零值
- n,ok := <-ch1,当ch被关闭后,m将被赋值为ch元素类型的零值, ok值为false
- for v := range ch,当ch被关闭后,for range循环结束
- select:同时对多个 channel 进行操作
select {
case x := <-ch1:
fmt.Println("get ch1:", x)
case y := <-ch1:
fmt.Println("get ch2:", y)
case <-time.After(time.Second * 3):
fmt.Println("out time")
}
对于channel的异常场景,通过下面的表格进行说明:
channel使用场景
无缓冲 channel
信号传递
无缓冲的channel在接收时如果没有数据会先阻塞,利用这特性,可以实现一对一的信号的传递:
type signal struct{}
func work(c chan signal) {
println("worker is working...")
time.Sleep(1 * time.Second)
c <- signal{}
}
func main() {
println("start a worker...")
c := make(chan signal)
go work(c)
<-c
fmt.Println("worker work done!")
}
当然也可以实现一对多的信号传递:利用channel在close时,<-ch会收到一个默认值。
type signal struct{}
var wg sync.WaitGroup
func main() {
fmt.Println("start a group of workers...")
groupSignal := make(chan signal)
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
<-groupSignal
fmt.Printf("worker %d: start to work...\n", i)
wg.Done()
}(i + 1)
}
fmt.Println("the group of workers start to work...")
close(groupSignal)
wg.Wait()
fmt.Println("the group of workers work done!")
}
替代锁机制
可以利用无缓冲channel的阻塞性质实现临界区的功能,开启一个gorouting接收加锁信号,当收到信号,就可以运行下面的工作,运行完成再把消息返回给调用端,继续执行后面的代码:
type counter struct {
c1 chan int
c2 chan int
i int
}
func main() {
cter := &counter{
c1: make(chan int),
c2: make(chan int),
}
go func() {
for {
cter.c1 <- 1
// 加锁的操作
cter.i++
cter.c2 <- 1
}
}()
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(i int) {
// 开始加锁
<-cter.c1
// 运行完成
<-cter.c2
fmt.Println(cter.i)
wg.Done()
}(i)
}
wg.Wait()
}
带缓冲 channel
消息队列
channel 的原生特性与消息队列十分相似,包括 Goroutine 安全、有 FIFO(first-in, first out)保证等。
- 无论是 1 收 1 发还是多收多发,带缓冲 channel 的收发性能都要好于无缓冲 channel;
- 对于带缓冲 channel 而言,发送与接收的 Goroutine 数量越多,收发性能会有所下降;
- 对于带缓冲 channel 而言,选择适当容量会在一定程度上提升收发性能。
计数信号量
固定容量的channel相当于信号量的大小,请求信号量相当于向channel发送消息,缓冲满了会阻塞,接收channel消息相当于释放信号量。
func main() {
active := make(chan struct{}, 3)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(j int) {
active <- struct{}{}
fmt.Printf("handle job: %d\n", j)
time.Sleep(2 * time.Second)
<-active
wg.Done()
}(i)
}
wg.Wait()
}
nil channel
channel 类型变量的值为 nil,读写都会发生阻塞。利用这个特定,当我们关闭channel后,它仍然可以就收空值,影响select的判断,我们可以将关闭的channel赋值为空,这样子就直接阻塞掉了。
ch1, ch2 := make(chan int), make(chan int)
go func() {
time.Sleep(time.Second * 5)
ch1 <- 5
close(ch1)
}()
go func() {
time.Sleep(time.Second * 7)
ch2 <- 7
close(ch2)
}()
for {
select {
case x, ok := <-ch1:
if !ok {
ch1 = nil
} else {
fmt.Println(x)
}
case x, ok := <-ch2:
if !ok {
ch2 = nil
} else {
fmt.Println(x)
}
}
if ch1 == nil && ch2 == nil {
break
}
}
fmt.Println("program end")
Select
default 分支避免阻塞
其他非 default 分支因通信未就绪,而无法被选择的时候执行的,这就给 default 分支赋予了一种“避免阻塞”的特性。
func tryRecv(c <-chan int) (int, bool) {
select {
case i := <-c:
return i, true
default:
return 0, false
}
}
func trySend(c chan<- int, i int) bool {
select {
case c <- i:
return true
default:
return false
}
}
实现超时机制
select {
case <-c:
// ... do some stuff
case <-time.After(30 *time.Second):
return
}
特别注意 timer 使用后的释放,尤其在大量创建 timer 的时候。
实现心跳机制
结合 time 包的 Ticker,我们可以实现带有心跳机制的 select。这种机制让我们可以在监听 channel 的同时,执行一些周期性的任务:
heartbeat := time.NewTicker(30 * time.Second)
defer heartbeat.Stop()
for {
select {
case <-c:
// ... do some stuff
case <- heartbeat.C:
//... do heartbeat stuff
}
}
用完 ticker 之后,也不要忘记调用它的 Stop 方法,避免心跳事件在 ticker 的 channel(上面示例中的 heartbeat.C)中持续产生。