简析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语言学习笔记、语法知识、技术要点和个人理解及实战