🐉大家好,我是gopher_looklook,现任某独角兽企业Go语言工程师,喜欢钻研Go源码,发掘各项技术在大型Go微服务项目中的最佳实践,期待与各位小伙伴多多交流,共同进步!
看过Go面经或参加过Go面试的小伙伴应该都碰到过一道很经典的面试手撕题——两个协程交替打印。我以前面试过深信服Golang工程师,当时面试官就让我手撕过这道题,这篇博客和大家探讨一下这道经典面试题的几种解法。
-
题目描述:使用两个go协程,交替打印26个字母和数字,输出结果: A1B2...Z26。
-
题目解析:解题关键在于当我们开了两个协程时,如何控制协程1打印一个字母后,暂停执行,通知到协程2打印数字。当协程2打印完数字后,暂停执行,反过来通知到协程1打印下一个字母。
基于上述分析思路,我想到的解题方式有下面几种:
解法一:使用两个channel
- 代码
package main
import (
"fmt"
"sync"
)
// 协程1
func printLetters(ch1, ch2 chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
for i := 'A'; i <= 'Z'; i++ {
<-ch1
fmt.Printf("%c", i)
ch2 <- struct{}{}
}
<-ch1
}
// 协程2
func printNumbers(ch1, ch2 chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 26; i++ {
<-ch2
fmt.Printf("%d", i)
ch1 <- struct{}{}
}
}
func main() {
var wg sync.WaitGroup
ch1 := make(chan struct{})
ch2 := make(chan struct{})
wg.Add(2)
go printNumbers(ch1, ch2, &wg)
go printLetters(ch1, ch2, &wg)
ch1 <- struct{}{}
wg.Wait()
}
- 解析:首先定义两个无缓冲的channel: ch1和ch2。开两个go协程分别循环打印26个字母和数字。在循环体里,每次打印之前,都从无缓冲的channel里读取数据。在未给ch1和ch2传入数据时,两个协程for循环的第一次执行,都会由于无法从无缓冲的channel里获取数据而阻塞。当我们在主函数里执行
ch1 <- struct{}{}
,协程1的阻塞得以释放,顺利打印出第一个字母。打印完成后,执行ch2 <- struct{}{}
,此时下一个for循环由于ch1没有数据被阻塞。而协程2由于监听到ch2传入数据,停止阻塞并打印第一个数字。打印完毕后,执行ch1 <- struct{}{}
,下一次for循环由于ch2没有数据被阻塞,却又释放了协程1,打出第二个字母。以此类推,两个协程循环往复地交替打印着。当打印完最后一个数字26时,ch2不再有协程收发数据(两个for循环都执行完成),但ch1会最后存入一次数据,所以要在协程1最后,从ch1中最后接收一次数据,避免程序死锁。
解法二:sync.Mutex和sync.Cond
- 代码
package main
import (
"fmt"
"sync"
)
// 协程1
func printLetters(cond *sync.Cond, turn *bool, wg *sync.WaitGroup) {
defer wg.Done()
for i := 'A'; i <= 'Z'; i++ {
cond.L.Lock()
for !*turn {
cond.Wait()
}
fmt.Printf("%c", i)
*turn = false
cond.Signal()
cond.L.Unlock()
}
}
// 协程2
func printNumbers(cond *sync.Cond, turn *bool, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 26; i++ {
cond.L.Lock()
for *turn {
cond.Wait()
}
fmt.Printf("%d", i)
*turn = true
cond.Signal()
cond.L.Unlock()
}
}
func main() {
var mu sync.Mutex
var cond = sync.NewCond(&mu)
var wg sync.WaitGroup
turn := false
wg.Add(2)
// 先设置 turn 为 false 并广播唤醒所有等待的协程
turn = true
go printLetters(cond, &turn, &wg)
//time.Sleep(time.Second)
go printNumbers(cond, &turn, &wg)
wg.Wait()
}
- 解析:这段代码利用到了
sync.Cond
可以阻塞/唤醒goroutine的机制。同样定义两个协程,分别打印字母和数字。传入一个共享的控制for循环打印的bool类型参数turn。协程1在turn为true时打印字母,为false时阻塞当前goroutine;协程2在turn为false时打印数字,为true时阻塞当前goroutine。初始化turn为true,之后开始执行两个协程,协程1打印完第一个字母后,修改turn为false,唤醒协程2之后,自身陷入阻塞;协程2打印完第一个数字后,修改turn为true,唤醒协程1之后,自身陷入阻塞。以此类推,两个协程循环往复地交替打印着,达到我们想要的效果。
解法三:select + channel
- 代码
package main
import (
"fmt"
"sync"
)
// 协程1
func printLetters(ch1, ch2 chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
for i := 'A'; i <= 'Z'; i++ {
select {
case <-ch1:
fmt.Printf("%c", i)
ch2 <- struct{}{}
}
}
<-ch1
}
// 协程2
func printNumbers(ch1, ch2 chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
for i := 1; i <= 26; i++ {
select {
case <-ch2:
fmt.Printf("%d", i)
ch1 <- struct{}{}
}
}
}
func main() {
var wg sync.WaitGroup
ch1 := make(chan struct{})
ch2 := make(chan struct{})
wg.Add(2)
go printLetters(ch1, ch2, &wg)
go printNumbers(ch1, ch2, &wg)
ch1 <- struct{}{}
wg.Wait()
}
- 解析:解法3和解法1有异曲同工之处,依旧是利用了两个无缓冲channel,阻塞一个时释放另一个,以此达到交替打印的目的。阻塞的方式,则是利用到了
select
监听channel的能力。任何一个协程的select得以执行时,一定会阻塞当前go协程for循环的执行,完成打印任务后,利用channel释放另一个协程正在阻塞的select操作。
总结
- 以上就是博主对于
两个协程交替打印
这道经典面试题的思考和答案。当年面试深信服的时候,博主采用了解法1的形式,还是比较顺利地解答出来了。看到这篇文章的小伙伴们,如果有更好的思路,欢迎在评论区留言和交流!