Go语言高频面试题—两个协程交替打印
大家好,我是gopher_looklook,现任某独角兽企业后端开发工程师,喜欢钻研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的形式,还是比较顺利地解答出来了。看到这篇文章的小伙伴们,如果有更好的思路,欢迎在评论区留言和交流!