[Goroutine]使用多协程并发地按照顺序打印字母表

今天分享一道非常经典的并发问题,使用多个协程按照顺序打印字母表的字母,每个打印 10 次。

思路:显然这里是要我们管道和协程完成同步交替打印,先把问题缩小,思考三个协程打印 a、b、c 的情形。最直接的思路就是定义三个管道,第一个协程打印完之后之后通知下一个协程,最后一个协程打印完成之后通知第一个协程继续打印,从而形成一个环。

代码如下:

go 复制代码
// 使用三个管道实现三个协程同步顺序打印a b c  
func printLetter(letter string, prevCh, nextCh chan struct{}, wg *sync.WaitGroup) {  
    defer wg.Done()  

    for i := 0; i < 10; i++ {  
        // 等待上一个协程通知  
        <-prevCh  
        fmt.Print(letter)  
        // 发送信号给下一个协程  
        nextCh <- struct{}{}  
    }
}

func main() {
    var wg sync.WaitGroup  
    wg.Add(3)  

    ch1 := make(chan struct{})  
    ch2 := make(chan struct{})  
    ch3 := make(chan struct{})  

    go printLetter("a", ch1, ch2, &wg)  
    go printLetter("b", ch2, ch3, &wg)  
    go printLetter("c", ch3, ch1, &wg)  

    // 启动第一个协程  
    ch1 <- struct{}{}  

    wg.Wait()
}

运行代码你会惊奇的发现最终结果是打印出来了,但是出现了死锁问题。对于有技术追求的程序员来说,怎么能就这样算了呢,肯定要给他解决了。

分析问题:问题的根源就是我们在通知下一个协程打印字母时,最后会形成一个环形,那么在第 1 个,第二个协程打印结束之后就会退出,最后一个协程在打印完成之后会管道 ch1 做 ch1 <- struct{}{} 的操作。因为我们定义的是无缓冲管道 ,所以第 3 个协程会立刻阻塞,但是第一个协程已经退出了没有办法对 ch1 做 <-ch1 操作,所以最后一个协程就会一直阻塞,WaitGroup 的计数器一直无法置零主协程无法退出,最终导致最后一个协程和主协程之间形成死锁,程序崩溃。

解决方法也很简单,只要在 printLetter 函数中加一个判断,判断它是否是第一个协程,如果是那么就对 prevCh 做 <-prevCh 操作以避免死锁问题。

go 复制代码
func printLetter(letter string, prevCh, nextCh chan struct{}, wg *sync.WaitGroup) {  
    defer wg.Done()  

    for i := 0; i < 10; i++ {  
        // 等待上一个协程通知  
        <-prevCh  
        fmt.Print(letter)  
        // 发送信号给下一个协程  
        nextCh <- struct{}{}  
    }
    
    if letter == "a" {
        <-prevCh
    }
}

这样第 1 个协程必须得等最后一个协程做 nextCh <- struct{}{} 操作才能退出,或者说最后一个协程等待第 1 个协程做 <-prevCh 操作才能退出。最终主协程也可以安全地退出。

对于使用多协程顺序打印字母表的问题,相信你读到这里也有思路了吧,代码如下:

go 复制代码
// 使用26个协程分别顺序打印字母表  
func printAlphabet(letter rune, prevCh, nextCh chan struct{}, wg *sync.WaitGroup) {  
    defer wg.Done()
    for i := 0; i < 10; i++ {  
        <-prevCh
        fmt.Printf("%c", letter)
        nextCh <- struct{}{}
    }  
    // 第一个协程必须要退出,因为最后一个协程往管道里面写入数据了,需要破环而出不然就会死锁  
    if letter == 'a' {
        <-prevCh
    }
}
  
func main() {  
    var wg sync.WaitGroup
    wg.Add(26)

    var signals []chan struct{}  
    for i := 0; i < 26; i++ {  
        signals = append(signals, make(chan struct{}))  
    }  

    for letter, i := 'a', 0; letter <= 'z'; letter++ {  
        if letter == 'z' {  
            go printAlphabet(letter, signals[i], signals[0], &wg)
            break
        }
        go printAlphabet(letter, signals[i], signals[i+1], &wg)  
        i++
    }

    // 启动第一个协程  
    signals[0] <- struct{}{}  
    wg.Wait()  
}

这里我使用了一个切片存储了 26 个管道,这样避免了写重复代码。最终还是跟上面的代码一样,最后一个协程得要等第 1 个协程一起退出才不会死锁。

相关推荐
一 乐19 小时前
婚纱摄影网站|基于ssm + vue婚纱摄影网站系统(源码+数据库+文档)
前端·javascript·数据库·vue.js·spring boot·后端
码事漫谈20 小时前
Protocol Buffers 编码原理深度解析
后端
码事漫谈20 小时前
gRPC源码剖析:高性能RPC的实现原理与工程实践
后端
踏浪无痕1 天前
AI 时代架构师如何有效成长?
人工智能·后端·架构
程序员小假1 天前
我们来说一下无锁队列 Disruptor 的原理
java·后端
辞砚技术录1 天前
MySQL面试题——联合索引
数据库·面试
小L~~~1 天前
绿盟校招C++研发工程师一面复盘
c++·面试
武子康1 天前
大数据-209 深度理解逻辑回归(Logistic Regression)与梯度下降优化算法
大数据·后端·机器学习
maozexijr1 天前
Rabbit MQ中@Exchange(durable = “true“) 和 @Queue(durable = “true“) 有什么区别
开发语言·后端·ruby
源码获取_wx:Fegn08951 天前
基于 vue智慧养老院系统
开发语言·前端·javascript·vue.js·spring boot·后端·课程设计