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 交替打印数字的功能。

相关推荐
猷咪24 分钟前
C++基础
开发语言·c++
IT·小灰灰25 分钟前
30行PHP,利用硅基流动API,网页客服瞬间上线
开发语言·人工智能·aigc·php
快点好好学习吧27 分钟前
phpize 依赖 php-config 获取 PHP 信息的庖丁解牛
android·开发语言·php
秦老师Q27 分钟前
php入门教程(超详细,一篇就够了!!!)
开发语言·mysql·php·db
烟锁池塘柳028 分钟前
解决Google Scholar “We‘re sorry... but your computer or network may be sending automated queries.”的问题
开发语言
是誰萆微了承諾28 分钟前
php 对接deepseek
android·开发语言·php
2601_9498683631 分钟前
Flutter for OpenHarmony 电子合同签署App实战 - 已签合同实现
java·开发语言·flutter
yyy(十一月限定版)1 小时前
寒假集训4——二分排序
算法
星火开发设计1 小时前
类型别名 typedef:让复杂类型更简洁
开发语言·c++·学习·算法·函数·知识
醉颜凉1 小时前
【LeetCode】打家劫舍III
c语言·算法·leetcode·树 深度优先搜索·动态规划 二叉树