简析go语言无缓冲channel
无缓冲channel兼具通信和同步特性,在并发程序中应用颇为广泛。可以通过不带有capacity参数的内置make函数创建一个可用的无缓冲channel:c := make(chan T)。
由于无缓冲channel的运行时层实现不带有缓冲区,因此对无缓冲channel的接收和发送操作是同步的,即对于同一个无缓冲channel,只有在对其进行接收操作的goroutine和对其进行发送操作的goroutine都存在的情况下,通信才能进行,否则单方面的操作会让对应的goroutine陷入阻塞状态。
对于无缓冲channel的操作时序有以下两点:
1.发送动作一定发生在接收动作完成之前
2.接收动作一定发生在发送动作完成之前
var c = make(chan int)
var s string
func f() {
s = "hello world"
<-c
}
func main() {
go f()
c <- 5
println(s)
}
因为f中的channel接收动作发生在主goroutine对channel发送动作完成之前, f函数中s = "hello world"又发生在channel接收动作之前,因此主goroutine在channel发生操作完成后输出的变量s一定是"hello world"。
无缓冲channel的使用场景如下:
- 一对一通知信号:无缓冲channel常被用于两个goroutine之间一对一地传递通知信号。
import "time"
type signal struct{}
func worker() {
println("do work...")
time.Sleep(1 * time.Second)
}
func spawn(f func()) <-chan signal {
c := make(chan signal)
go func() {
println("start a work")
f()
c <- signal{}
}()
return c
}
func main() {
c := spawn(worker)
<-c
println("work done")
}
$ go run channel-case.go
start a work
do work...
work done
该例中spawn函数返回的channel被用于承载新goroutine退出的通知信号。该信号专用于通知main goroutine。main goroutine在调用spawn函数后一直阻塞在对这个通知信号的接收动作上。
2.一对多通知信号:有些时候,无缓冲channel还被用于实现一对多的信号通知机制,这样的信号通知机制常被用于协调多个goroutine一起工作。
import (
"fmt"
"sync"
"time"
)
type signal struct{}
func worker(i int) {
fmt.Printf("worker %d: start workering\n", i)
time.Sleep(1 * time.Second)
}
func spawnGroup(f func(i int), nums int, groupSignal <-chan signal) <-chan signal {
c := make(chan signal)
var wg sync.WaitGroup
for i := 0; i < nums; i++ {
wg.Add(1)
go func(i int) {
<-groupSignal
fmt.Printf("worker %d: start worke\n", i)
f(i)
fmt.Printf("worker %d: work done\n", i)
wg.Done()
}(i + 1)
}
go func() {
wg.Wait()
c <- signal{}
}()
return c
}
func main() {
groupSignal := make(chan signal)
fmt.Println("start a group of workers")
c := spawnGroup(worker, 5, groupSignal)
time.Sleep(5 * time.Second)
close(groupSignal)
<-c
fmt.Println("a group of workers done")
}
$ go run channel-case.go
start a group of workers
worker 4: start worke
worker 4: start workering
worker 1: start worke
worker 1: start workering
worker 2: start worke
worker 2: start workering
worker 3: start worke
worker 3: start workering
worker 5: start worke
worker 5: start workering
worker 5: work done
worker 4: work done
worker 3: work done
worker 2: work done
worker 1: work done
a group of workers done
上例中main goroutine创建了5个worker goroutine,这些goroutine启动后都阻塞在名为groupSignal的无缓冲channel上,main goroutine通过close(groupSignal)向所有worker goroutine广播”开始工作“的信号,所有worker goroutine在收到groupSignal信号后一起开始工作。就像起跑线上的运动员听到裁判员的起跑信号枪声后起跑一样。
我们看到,关闭一个channel会让所有阻塞在该channel上的接收操作返回,从而实现一种一对多的广播机制。该一对多的信号通知机制还常用于通知一组worker goroutine退出。
import (
"fmt"
"sync"
"time"
)
type signal struct{}
func worker(i int, quit <-chan signal) {
fmt.Printf("worker %d: start workering\n", i)
LOOP:
for {
select {
case <-quit:
break LOOP
default:
time.Sleep(1 * time.Second)
}
}
fmt.Printf("worker %d: work done\n", i)
}
func spawnGroup(f func(int, <-chan signal), nums int, groupSignal <-chan signal) <-chan signal {
c := make(chan signal)
var wg sync.WaitGroup
for i := 0; i < nums; i++ {
wg.Add(1)
go func(i int) {
fmt.Printf("worker %d: start worke\n", i)
f(i, groupSignal)
wg.Done()
}(i + 1)
}
go func() {
wg.Wait()
c <- signal{}
}()
return c
}
func main() {
groupSignal := make(chan signal)
fmt.Println("start a group of workers")
c := spawnGroup(worker, 5, groupSignal)
time.Sleep(5 * time.Second)
close(groupSignal)
<-c
fmt.Println("a group of workers done")
}
$ go run channel-case.go
start a group of workers
worker 4: start worke
worker 4: start workering
worker 5: start worke
worker 5: start workering
worker 3: start worke
worker 3: start workering
worker 2: start worke
worker 2: start workering
worker 1: start worke
worker 1: start workering
worker 3: work done
worker 5: work done
worker 1: work done
worker 2: work done
worker 4: work done
a group of workers done
Go语言学习笔记、语法知识、技术要点和个人理解及实战


查看13道真题和解析