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


theme: channing-cyan

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

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

代码如下:

```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()

} ```

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

image.png

分析问题:问题的根源就是我们在通知下一个协程打印字母时,最后会形成一个环形,那么在第 1 个,第 2 个协程打印结束之后就会退出,最后一个协程在打印完成之后会管道 ch1 做 ch1 <- struct{}{} 的操作。因为我们定义的是无缓冲管道,所以第 3 个协程会立刻阻塞,但是第 1 个协程已经退出了没有办法对 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 个协程一起退出才不会死锁。

猜你喜欢

转载自blog.csdn.net/weixin_45254062/article/details/132059697