[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 个协程一起退出才不会死锁。

相关推荐
自珍JAVA10 分钟前
Gobrs-Async 框架
后端
xdscode16 分钟前
Spring 依赖注入方式全景解析
java·后端·spring
青柠代码录24 分钟前
【Spring】@Component VS @Configuration
后端
喵个咪1 小时前
go-wind-cms 微服务架构设计:为什么基于 Kratos?
后端·微服务·cms
神奇小汤圆1 小时前
百度面试官:Redis 内存满了怎么办?你有想过吗?
后端
喵个咪1 小时前
Headless 架构优势:内容与展示解耦,一套 API 打通全端生态
前端·后端·cms
开心就好20251 小时前
HTTPS超文本传输安全协议全面解析与工作原理
后端·ios
小江的记录本1 小时前
【JEECG Boot】 JEECG Boot——数据字典管理 系统性知识体系全解析
java·前端·spring boot·后端·spring·spring cloud·mybatis
神奇小汤圆1 小时前
Spring Batch实战
后端
喵个咪1 小时前
传统 CMS 太笨重?试试 Headless 架构的 GoWind,轻量又强大
前端·后端·cms