• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

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

武飞扬头像
Code_Artist
帮助1

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

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

代码如下:

// 使用三个管道实现三个协程同步顺序打印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 操作以避免死锁问题。

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 操作才能退出。最终主协程也可以安全地退出。

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

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

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfkacai
系列文章
更多 icon
同类精品
更多 icon
继续加载