goroutine 交替打印奇偶数
题目
使用两个 goroutine
交替并且顺序地打印数字 1…10, 要求其中一个 goroutine
打印奇数,另一个 goroutine
打印偶数。
本文尝试通过 Go 标准库提供的 API 来给出几种解决方案,虽然这看上去有点儿 “八股文” 的味道,但是对于提高 goroutine + channel
编码技巧还是非常有帮助的。
channel 方法
我们可以直接使用 goroutine + channel
的方式,写出直观的代码:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 打印奇数
go func() {
for n := range ch1 {
fmt.Printf("channel-1 print %d\n", n)
// 奇数 + 1 等于下一个要打印的偶数
// 发送到 channel 2
ch2 <- n + 1
if n == 9 {
break
}
}
close(ch1)
}()
// 打印偶数
go func() {
for n := range ch2 {
fmt.Printf("channel-2 print %d\n", n)
if n == 10 {
break
}
// 偶数 + 1 等于下一个要打印的奇数
// 发送到 channel 1
ch1 <- n + 1
}
close(ch2)
}()
ch1 <- 1
// 等待 goroutine 执行完成
time.Sleep(time.Second)
}
运行代码打印如下:
channel-1 print 1
channel-2 print 2
channel-1 print 3
channel-2 print 4
channel-1 print 5
channel-2 print 6
channel-1 print 7
channel-2 print 8
channel-1 print 9
channel-2 print 10
channel 优化版本
上面的代码虽然可以正确运行,但是存在两个小问题:
- 两个
goroutine
代码块堆叠在一起,有些冗余和重复,可以重构为一个函数 - 使用
time.Sleep
方法等待goroutine
执行完成,不够优雅且存在很多潜在问题 (例如真实场景下我们得知goroutine
的具体运行时间),可以使用sync.WaitGroup
同步原语进行重构
优化过的代码如下:
package main
import (
"fmt"
"sync"
)
// rev 接收 channel
// send 发送 channel
// no channel 编号 (提高打印消息的可读性)
// last channel 接收的最后一个数字, 超过该数字时结束打印
func printChar(rev <-chan int, send chan<- int, no, last int) {
for i := range rev {
if i > last {
break
}
fmt.Printf("channel-%d print %d\n", no, i)
send <- i + 1
}
close(send)
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
var wg sync.WaitGroup
wg.Add(2)
// 打印奇数
go func() {
defer wg.Done()
printChar(ch1, ch2, 1, 9)
}()
// 打印偶数
go func() {
defer wg.Done()
printChar(ch2, ch1, 2, 10)
}()
ch1 <- 1
wg.Wait()
}
通过将打印过程封装为一个函数,然后交替传递 channel
参数,再使用 sync.WaitGroup
同步原语控制打印何时结束,避免了 time.Sleep
方法这种有明显缺陷的方案。
运行代码打印如下:
channel-1 print 1
channel-2 print 2
channel-1 print 3
channel-2 print 4
channel-1 print 5
channel-2 print 6
channel-1 print 7
channel-2 print 8
channel-1 print 9
channel-2 print 10
sync.Mutex 互斥锁
除了最基础的 channel
之外, 还可以使用标准库提供的同步原语来解决这个问题。
- 当打印奇数的
goroutine
获取到锁时,打印奇数并切换下一次打印的goroutine
编号,最后通过解锁操作来唤醒偶数goroutine
- 当打印偶数的
goroutine
获取到锁时,打印偶数并切换下一次打印的goroutine
编号,最后通过解锁操作来唤醒奇数goroutine
package main
import (
"fmt"
"sync"
)
// cond sync.Mutex 互斥锁
// cur 指定当前执行的 goroutine 编号
// no channel 编号 (提高打印消息的可读性)
// last channel 接收的最后一个数字, 超过该数字时结束打印
func printChar(mu *sync.Mutex, cur *int, no, last int) {
for i := no; i <= last; {
mu.Lock()
if *cur != no {
// 如果当前执行 goroutine 编号不是自身
mu.Unlock()
continue
}
fmt.Printf("channel-%d print %d\n", no, i)
// 指定当前执行的 goroutine 编号为另外一个 goroutine
*cur = 3 - *cur
i = i + 2
// 唤醒另外一个 goroutine
mu.Unlock()
}
}
func main() {
mu := &sync.Mutex{}
// 当前执行的 goroutine 编号
cur := 1
var wg sync.WaitGroup
wg.Add(2)
// 打印奇数
go func() {
defer wg.Done()
printChar(mu, &cur, 1, 9)
}()
// 打印偶数
go func() {
defer wg.Done()
printChar(mu, &cur, 2, 10)
}()
wg.Wait()
}
sync.Cond 条件变量
在互斥锁的代码基础上稍加改造,即可适配 sync.Cond
条件变量同步原语代码。
package main
import (
"fmt"
"sync"
)
// cond sync.Cond 条件变量
// cur 指定当前执行的 goroutine 编号
// no channel 编号 (提高打印消息的可读性)
// last channel 接收的最后一个数字, 超过该数字时结束打印
func printChar(cond *sync.Cond, cur *int, no, last int) {
for i := no; i <= last; i = i + 2 {
cond.L.Lock()
for *cur != no {
// 如果当前执行 goroutine 编号不是自身
// 等待被唤醒
cond.Wait()
}
fmt.Printf("channel-%d print %d\n", no, i)
// 指定当前执行的 goroutine 编号为另外一个 goroutine
*cur = 3 - *cur
// 唤醒另外一个 goroutine
cond.Signal()
cond.L.Unlock()
}
}
func main() {
cond := sync.NewCond(&sync.Mutex{})
// 当前执行的 goroutine 编号
cur := 1
var wg sync.WaitGroup
wg.Add(2)
// 打印奇数
go func() {
defer wg.Done()
printChar(cond, &cur, 1, 9)
}()
// 打印偶数
go func() {
defer wg.Done()
printChar(cond, &cur, 2, 10)
}()
wg.Wait()
}
小结
本文通过一个笔试题,给出了 Go 中 goroutine + channel
组合使用时常见的同步解决方案,希望能帮助读者在扩展 goroutine + channel
的基础应用同时,深入理解各同步原语之间的差异及其使用场景。