Golang控制协程执行顺序的三种实用方法

Ryan
Ryan
发布时间:2026-10-15 | 2人浏览过
原创

Go 语言的 Goroutine 默认由调度器随机分配执行,导致输出顺序不可控。若业务逻辑要求严格的串行或有序并发,可通过原子变量轮询、Channel 信道传递或 Mutex 锁链等方式实现顺序控制。

背景与问题

在 Go 的 GPM 调度模型中,协程(G)需要绑定到机器线程(M)上运行,并通过 P 的私有队列进行调度。由于不同队列间没有顺序约束,创建多个协程后,它们的执行顺序是随机的。但在某些场景下(如资源初始化、日志排序),必须确保特定的执行先后顺序。

方法一:基于原子变量的循环轮询

核心思路是为每个任务分配一个序号,仅允许当前序号对应的协程执行,执行完毕后递增序号。关键在于使用 sync/atomic 包保证计数的线程安全。

第一步:实现顺序控制函数

创建一个包装函数,内部包含循环逻辑。协程会不断尝试加载全局计数器,若匹配自身序号则执行逻辑并增加计数,否则短暂休眠重试。

var count uint32
func sequence(i uint32, fn func()) {
	for {
		// 使用原子操作读取当前计数
		if n := atomic.LoadUint32(&count); n == i {
			fn()
			// 执行完成后,原子递增计数
			atomic.AddUint32(&count, 1)
			break // 当前任务完成,退出循环
		}
		time.Sleep(time.Nanosecond) // 避免 CPU 空转
	}
}

第二步:在主程序中启动有序协程

将具体业务逻辑封装为 fn,传入 sequence 函数。主协程最后也会进入序列等待,确保所有子协程执行完毕后再退出。

func main() {
	var times uint32 = 5
	for i := uint32(0); i < times; i++ {
		go func(i uint32) {
			fn := func() {
				fmt.Printf("this i is %v\n", i)
			}
			sequence(i, fn)
		}(i)
	}
	// 让主协程等待最后一个任务完成
	sequence(times, func() {})
}

this i is 0
this i is 1
this i is 2
this i is 3
this i is 4

优点:简单直观。
缺点:存在忙等待(Busy Waiting),虽然加入了延时,但仍有少量 CPU 开销,不适合极高并发或性能敏感场景。

方法二:基于 Channel 的信道阻塞

利用 Go 语言最核心的通信机制 Channel。前一个协程执行结束后向下一个协程的通道发送信号(或关闭通道),后一个协程通过接收信号解除阻塞。

第一步:创建依赖链

为 N 个协程准备 N-1 个通道。第一个协程无需等待,后续协程依次监听前一个通道的状态。

func main() {
	c1 := make(chan struct{})
	c2 := make(chan struct{})
	c3 := make(chan struct{})

	go func() {
		// 协程一:直接执行,完成后关闭 c1
		fmt.Println("this value is 0")
		close(c1)
	}()

	go func() {
		// 协程二:等待 c1 关闭后执行,完成后关闭 c2
		<-c1
		fmt.Println("this value is 1")
		close(c2)
	}()

	go func() {
		// 协程三:等待 c2 关闭后执行
		<-c2
		fmt.Println("this value is 2")
		close(c3)
	}()

	// 主协程等待 c3 关闭,确保所有工作完成
	<-c3
}

原理:对已关闭的 Channel 进行接收操作会立即返回零值且无阻塞,从而触发下一个协程的执行。

方法三:基于互斥锁链的同步

使用一组预加锁的 Mutex 作为锁链。初始时所有锁均为“锁定”状态。第一个协程解锁入口锁开始执行,释放后同时解锁下一个锁,唤醒下一个协程。

第一步:初始化锁链

创建长度为 N+1 的 Mutex 数组,所有锁默认调用 Lock() 保持锁定状态。

第二步:编排协程逻辑

第 i 个协程等待第 i 号锁,执行完毕后,先解锁自己的锁(允许被重置或不再需要),然后解锁第 i+1 号锁,从而激活下一个协程。

func main() {
	times := 5
	// 创建 Mutex 数组,多一个用于主协程等待
	var cc = make([]*sync.Mutex, times+1)
	
	// 初始化:全部加锁
	for i := 0; i < len(cc); i++ {
		m := &sync.Mutex{}
		m.Lock()
		cc[i] = m
	}

	for i := 0; i < times; i++ {
		go func(index int) {
			// 等待前一个锁被释放
			cc[index].Lock()
			fmt.Printf("this value is %d \n", index)
			// 执行完成后,解锁自身位置并解锁下一位
			cc[index].Unlock()
			cc[index+1].Unlock()
		}(i)
	}

	// 解锁入口锁 (index 0),启动链条
	cc[0].Unlock()
	
	// 主协程等待最后一个锁被释放
	cc[times].Lock()
	cc[times].Unlock()
}

优势:无忙等待,CPU 利用率低,适合复杂依赖关系的同步控制。

总结

控制 Goroutine 执行顺序主要有三种方式:atomic 轮询适合简单轻量场景;Channel 语义清晰,符合 Go 哲学;Mutex 链式解锁效率高且扩展性强。选择哪种方案取决于具体的性能需求和代码复杂度。以上就是 Golang 控制协程执行顺序的详细方法,更多关于 Go 并发编程的资料请关注本站其它相关文章!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表