Golang控制协程执行顺序的三种实用方法
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 并发编程的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













