Go语言交替打印问题及多种实现方法

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 条件变量

思路:

  • 使用一个共享变量 counterturn 表示当前轮到哪个 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 交替打印数字的功能。

相关推荐
.格子衫.1 分钟前
真题卷001——算法备赛
算法
XiaoyaoCarter11 分钟前
每日一道leetcode
c++·算法·leetcode·职场和发展·二分查找·深度优先·前缀树
Blossom.11813 分钟前
使用Python实现简单的人工智能聊天机器人
开发语言·人工智能·python·低代码·数据挖掘·机器人·云计算
da-peng-song21 分钟前
ArcGIS Desktop使用入门(二)常用工具条——数据框工具(旋转视图)
开发语言·javascript·arcgis
galaxy_strive21 分钟前
qtc++ qdebug日志生成
开发语言·c++·qt
TNTLWT24 分钟前
Qt功能区:简介与安装
开发语言·qt
Hygge-star29 分钟前
【数据结构】二分查找5.12
java·数据结构·程序人生·算法·学习方法
等等5431 小时前
Java EE初阶——wait 和 notify
java·开发语言
低代码布道师2 小时前
第五部分:第一节 - Node.js 简介与环境:让 JavaScript 走进厨房
开发语言·javascript·node.js
June`2 小时前
专题二:二叉树的深度搜索(二叉树剪枝)
c++·算法·深度优先·剪枝