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的形式,还是比较顺利地解答出来了。看到这篇文章的小伙伴们,如果有更好的思路,欢迎在评论区留言和交流!

全部评论
m
点赞 回复 分享
发布于 01-27 19:25 河北
佬有无go学习交流社区啊,感觉遇到瓶颈了,go的学习路线也很少,一个人好难学下去啊
点赞 回复 分享
发布于 01-25 13:40 广东

相关推荐

1.&nbsp;自我介绍。2.&nbsp;实习经历。3.&nbsp;开源经历和要点(主要包括实现思路和优化)。4.&nbsp;执行一条&nbsp;SQL(select)&nbsp;语句,期间发生了什么?5.&nbsp;如何利用数据库索引?6.&nbsp;题目一(SQL):表&nbsp;students&nbsp;包含字段&nbsp;stu_id,class_id,name&nbsp;其中&nbsp;stu_id&nbsp;是不重复的,每个&nbsp;stu_id&nbsp;对应一个学生,每个学生只能在一个班级中。1、请写出&nbsp;sql,统计每个班的学生数量,查询结果&nbsp;的列名为&nbsp;class_id,count。2、请写出&nbsp;sql,统计学生数量大于&nbsp;10&nbsp;的班级,查询结果的列名为&nbsp;class_id,count。7.&nbsp;linux&nbsp;常用命令。8.&nbsp;查询某个文件某个关键字用到命令。9.&nbsp;题目二(Shell):某个文件一共十行,每一行依次是1~10,使用&nbsp;Shell&nbsp;脚本完成文件内容输出到控制台打印。10.&nbsp;如何查看&nbsp;linux&nbsp;进程。11.&nbsp;对于&nbsp;kubernetes&nbsp;的了解...12.&nbsp;prometheus&nbsp;监控,关于如何配置&nbsp;prometheus&nbsp;的指标采集和上报?13.&nbsp;go&nbsp;的&nbsp;context&nbsp;是什么?有什么应用场景?14.&nbsp;对于&nbsp;docker&nbsp;的了解...15.&nbsp;举例&nbsp;docker&nbsp;常用的命令,详细解释&nbsp;docker&nbsp;tag。16.&nbsp;题目三(go&nbsp;-&nbsp;并发编程):使用多线程或协程或其他阻塞的方式,实现两个线程/协程对同一个变量进行加&nbsp;1&nbsp;操作,分别操作&nbsp;500&nbsp;万次,保证最后能够输出&nbsp;1000&nbsp;万。17.&nbsp;题目四(leetcode&nbsp;206.&nbsp;反转链表)18.&nbsp;描述&nbsp;zookeeper&nbsp;如何实现分布式锁?19.&nbsp;描述&nbsp;redis&nbsp;集群如何选取主节点?反问业务和后续流程
查看20道真题和解析
点赞 评论 收藏
分享
07-09 15:14
南京大学 C++
点赞 评论 收藏
分享
头像
06-25 17:05
南京大学 Java
投递阿里巴巴集团等公司9个岗位
点赞 评论 收藏
分享
评论
4
26
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务