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

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

相关推荐

评论
4
19
分享

创作者周榜

更多
牛客网
牛客企业服务