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 退出。
go
package main
import (
"fmt"
"sync"
)
func main() {
const max = 30
const n = 3 // goroutine 数量
channels := make([]chan bool, n)
for i := 0; i < n; i++ {
channels[i] = make(chan bool)
}
var wg sync.WaitGroup
wg.Add(n)
counter := 1
var mu sync.Mutex
done := make(chan struct{})
for i := 0; i < n; i++ {
go func(id int) {
defer wg.Done()
for {
select {
case <-done:
return
case _, 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)
}
// 启动第一个 goroutine
channels[0] <- true
wg.Wait()
for i := 0; i < n; i++ {
close(channels[i])
}
fmt.Println("打印结束")
}
方法二:使用单个 Channel 和 goroutine ID 控制
思路:
- 使用一个 channel 传递当前应该打印的 goroutine ID。
- 每个 goroutine 监听 channel,只有当收到的 ID 与自己相同时才打印数字。
- 打印后将下一个 goroutine 的 ID 发送回 channel。
go
package main
import (
"fmt"
"sync"
)
func main() {
const max = 30
const n = 3
ch := make(chan int)
var wg sync.WaitGroup
wg.Add(n)
counter := 1
var mu sync.Mutex
for i := 0; i < n; i++ {
go func(id int) {
defer wg.Done()
for {
curID := <-ch
if curID != id {
// 不是自己的轮次,放回去
ch <- curID
continue
}
mu.Lock()
if counter > max {
mu.Unlock()
// 结束所有 goroutine
// 发送特殊值 -1 表示结束
ch <- -1
return
}
fmt.Printf("线程 %d 打印 %d\n", id+1, counter)
counter++
mu.Unlock()
// 发送下一个 goroutine 的 ID
ch <- (id + 1) % n
}
}(i)
}
// 启动第一个 goroutine
ch <- 0
wg.Wait()
fmt.Println("打印结束")
}
方法三:使用 sync.Cond 条件变量
思路:
- 使用一个共享变量
counter
和turn
表示当前轮到哪个 goroutine 打印。 - 使用
sync.Cond
来等待和通知 goroutine。 - 每个 goroutine 等待条件满足(轮到自己),打印数字后更新
turn
并通知其他 goroutine。
go
package main
import (
"fmt"
"sync"
)
func main() {
const max = 30
const n = 3
var mu sync.Mutex
cond := sync.NewCond(&mu)
counter := 1
turn := 0
var wg sync.WaitGroup
wg.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) % n
mu.Unlock()
cond.Broadcast()
}
}(i)
}
wg.Wait()
fmt.Println("打印结束")
}
方法四:使用 Channel + select + 超时退出
思路:
- 使用一个 channel 传递打印任务。
- 每个 goroutine 监听 channel,只有当任务分配给自己时打印。
- 使用超时机制防止死锁。
go
package main
import (
"fmt"
"time"
)
func main() {
const max = 30
const n = 3
type task struct {
id int
counter 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 <- t
continue
}
if t.counter > max {
// 结束信号,放回去让其他 goroutine 退出
ch <- t
return
}
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 交替打印数字的功能。