Go語言交替打印問題及多種實現方法
在并發編程中,多個線程(或 goroutine)交替執行任務是一個經典問題。本文將以 Go 語言為例,介紹如何實現多個 goroutine 交替打印數字的功能,并展示幾種不同的實現方法。
Go 語言相關知識點
1. Goroutine
Goroutine 是 Go 語言的輕量級線程,使用 go
關鍵字啟動。它們由 Go 運行時調度,能夠高效地并發執行任務。
2. Channel
Channel 是 Go 語言中用于 goroutine 之間通信的管道。通過 channel,goroutine 可以發送和接收數據,實現同步和通信。
chan T
表示傳輸類型為T
的 channel。- 發送數據:
ch <- value
- 接收數據:
value := <- ch
3. sync.Mutex
互斥鎖,用于保護共享資源,防止多個 goroutine 同時訪問導致數據競爭。
4. sync.WaitGroup
用于等待一組 goroutine 完成。通過 Add
設置計數,Done
表示完成,Wait
阻塞直到計數歸零。
需求描述
- 有
n
個 goroutine(線程),編號從 1 到 n。 - 這 n 個 goroutine 交替打印數字,從 1 打印到
max
。 - 例如,3 個 goroutine,打印 1,2,3,4,…30,線程1打印1,線程2打印2,線程3打印3,線程1打印4,依次循環。
方法一:使用多個 Channel 輪流通知(基于題主代碼)
思路:
- 創建
n
個 channel,分別對應每個 goroutine。 - 每個 goroutine 等待自己的 channel 收到信號后打印數字,然后通知下一個 goroutine。
- 使用互斥鎖保護共享計數器。
- 使用一個
done
channel 通知所有 goroutine 退出。
package mainimport ("fmt""sync"
)func main() {const max = 30const n = 3 // goroutine 數量channels := make([]chan bool, n)for i := 0; i < n; i++ {channels[i] = make(chan bool)}var wg sync.WaitGroupwg.Add(n)counter := 1var mu sync.Mutexdone := make(chan struct{})for i := 0; i < n; i++ {go func(id int) {defer wg.Done()for {select {case <-done:returncase _, ok := <-channels[id]:if !ok {return}mu.Lock()if counter > max {mu.Unlock()close(done)return}fmt.Printf("線程 %d 打印 %d\n", id+1, counter)counter++mu.Unlock()channels[(id+1)%n] <- true}}}(i)}// 啟動第一個 goroutinechannels[0] <- truewg.Wait()for i := 0; i < n; i++ {close(channels[i])}fmt.Println("打印結束")
}
方法二:使用單個 Channel 和 goroutine ID 控制
思路:
- 使用一個 channel 傳遞當前應該打印的 goroutine ID。
- 每個 goroutine 監聽 channel,只有當收到的 ID 與自己相同時才打印數字。
- 打印后將下一個 goroutine 的 ID 發送回 channel。
package mainimport ("fmt""sync"
)func main() {const max = 30const n = 3ch := make(chan int)var wg sync.WaitGroupwg.Add(n)counter := 1var mu sync.Mutexfor i := 0; i < n; i++ {go func(id int) {defer wg.Done()for {curID := <-chif curID != id {// 不是自己的輪次,放回去ch <- curIDcontinue}mu.Lock()if counter > max {mu.Unlock()// 結束所有 goroutine// 發送特殊值 -1 表示結束ch <- -1return}fmt.Printf("線程 %d 打印 %d\n", id+1, counter)counter++mu.Unlock()// 發送下一個 goroutine 的 IDch <- (id + 1) % n}}(i)}// 啟動第一個 goroutinech <- 0wg.Wait()fmt.Println("打印結束")
}
方法三:使用 sync.Cond 條件變量
思路:
- 使用一個共享變量
counter
和turn
表示當前輪到哪個 goroutine 打印。 - 使用
sync.Cond
來等待和通知 goroutine。 - 每個 goroutine 等待條件滿足(輪到自己),打印數字后更新
turn
并通知其他 goroutine。
package mainimport ("fmt""sync"
)func main() {const max = 30const n = 3var mu sync.Mutexcond := sync.NewCond(&mu)counter := 1turn := 0var wg sync.WaitGroupwg.Add(n)for i := 0; i < n; i++ {go func(id int) {defer wg.Done()for {mu.Lock()for turn != id && counter <= max {cond.Wait()}if counter > max {mu.Unlock()cond.Broadcast()return}fmt.Printf("線程 %d 打印 %d\n", id+1, counter)counter++turn = (turn + 1) % nmu.Unlock()cond.Broadcast()}}(i)}wg.Wait()fmt.Println("打印結束")
}
方法四:使用 Channel + select + 超時退出
思路:
- 使用一個 channel 傳遞打印任務。
- 每個 goroutine 監聽 channel,只有當任務分配給自己時打印。
- 使用超時機制防止死鎖。
package mainimport ("fmt""time"
)func main() {const max = 30const n = 3type task struct {id intcounter int}ch := make(chan task)for i := 0; i < n; i++ {go func(id int) {for {select {case t := <-ch:if t.id != id {// 不是自己的任務,放回去ch <- tcontinue}if t.counter > max {// 結束信號,放回去讓其他 goroutine 退出ch <- treturn}fmt.Printf("線程 %d 打印 %d\n", id+1, t.counter)time.Sleep(100 * time.Millisecond) // 模擬工作ch <- task{id: (id + 1) % n, counter: t.counter + 1}case <-time.After(2 * time.Second):// 超時退出return}}}(i)}// 啟動第一個任務ch <- task{id: 0, counter: 1}// 等待足夠時間讓所有打印完成time.Sleep(5 * time.Second)fmt.Println("打印結束")
}
總結
- Go 語言提供了多種并發原語,能夠靈活實現線程間的協作。
- Channel 是 goroutine 通信的核心,適合用于事件通知和數據傳遞。
- sync.Mutex 和 sync.Cond 適合保護共享資源和實現復雜的同步邏輯。
- 選擇哪種方法取決于具體需求和代碼風格。
通過以上幾種方法,你可以根據實際場景選擇合適的實現方式,實現多個 goroutine 交替打印數字的功能。