
本文探讨了在go语言中如何利用通道(channels)实现不同协程间关键代码段的严格交替执行。通过构建一个“传球”机制,每个协程在完成其关键操作后将控制权传递给下一个协程,从而确保关键代码段以精确的顺序cs1、cs2、cs1、cs2等交替执行。这种模式具有良好的同步性、可扩展性,是go并发编程中解决特定顺序执行问题的有效方案。
在Go语言的并发编程中,协程(goroutines)的调度通常由运行时(runtime)负责,其执行顺序是不确定的。然而,在某些特定场景下,我们可能需要严格控制不同协程中特定代码段(即“关键代码段”)的执行顺序,例如要求它们必须交替执行:CS1、CS2、CS1、CS2,以此类推。
考虑以下两个协程函数f1和f2,它们各自包含一个关键代码段(CS1和CS2):
func f1() {
// ... some code
// critical section 1 (CS1)
// ... critical section code
// end critical section 1
// ... more code
}
func f2() {
// ... some code
// critical section 2 (CS2)
// ... critical section code
// end critical section 2
// ... more code
}
func main() {
go f1()
go f2()
// ...
}直接启动f1和f2协程无法保证CS1和CS2的交替执行。为了实现这种严格的交替顺序,我们需要一种有效的同步机制。
基于通道的“传球”机制
Go语言中的通道(channels)是协程间通信和同步的核心工具。我们可以利用无缓冲通道(unbuffered channels)的阻塞特性,设计一种“传球”机制来强制关键代码段的交替执行。其核心思想是:
立即学习“go语言免费学习笔记(深入)”;
- 每个协程都拥有一个“接收球”的通道和一个“传球”的通道。
- 一个协程只有在从其“接收球”通道接收到信号(即“球”)后,才能进入并执行其关键代码段。
- 关键代码段执行完毕后,该协程会向下一个协程的“接收球”通道发送一个信号(“传球”),从而允许下一个协程开始执行。
- 通过这种循环传递,确保了关键代码段的严格交替执行。
实现细节与示例代码
下面是使用这种“传球”机制实现f1和f2关键代码段交替执行的Go语言代码示例:
package main
import (
"fmt"
"time"
"sync" // 用于等待协程完成
)
// f1 协程函数,接收一个通道用于启动,发送一个通道用于传递控制权
func f1(do chan bool, next chan bool, wg *sync.WaitGroup) {
defer wg.Done() // 确保协程结束时通知 WaitGroup
for i := 0; i < 5; i++ { // 循环执行5次,模拟多次交替
<-do // 等待“球”,阻塞直到从do通道接收到值
fmt.Println("f1: Entering Critical Section 1 (CS1)")
// ... critical section code for f1
time.Sleep(100 * time.Millisecond) // 模拟关键代码段的执行时间
fmt.Println("f1: Exiting Critical Section 1 (CS1)")
next <- true // 将“球”传递给下一个协程
}
}
// f2 协程函数,接收一个通道用于启动,发送一个通道用于传递控制权
func f2(do chan bool, next chan bool, wg *sync.WaitGroup) {
defer wg.Done() // 确保协程结束时通知 WaitGroup
for i := 0; i < 5; i++ { // 循环执行5次,模拟多次交替
<-do // 等待“球”,阻塞直到从do通道接收到值
fmt.Println("f2: Entering Critical Section 2 (CS2)")
// ... critical section code for f2
time.Sleep(100 * time.Millisecond) // 模拟关键代码段的执行时间
fmt.Println("f2: Exiting Critical Section 2 (CS2)")
next <- true // 将“球”传递给下一个协程
}
}
func main() {
cf1 := make(chan bool, 1) // f1的启动通道
cf2 := make(chan bool, 1) // f2的启动通道
var wg sync.WaitGroup // 用于等待所有协程完成
wg.Add(2) // 增加计数器,表示有两个协程需要等待
// 启动 f1 和 f2 协程
go f1(cf1, cf2, &wg)
go f2(cf2, cf1, &wg)
// 初始状态:将“球”放入 cf1,让 f1 先启动
cf1 <- true
wg.Wait() // 等待所有协程完成,防止主协程过早退出
fmt.Println("All critical sections executed alternately.")
}代码解析:
- 通道创建: cf1和cf2是两个无缓冲布尔型通道。无缓冲通道意味着发送操作会阻塞直到有接收者准备好,反之亦然,这正是实现严格同步的关键。
- 协程函数签名: f1和f2都接收两个通道参数:do用于接收信号以开始执行,next用于发送信号以传递控制权。我们还加入了*sync.WaitGroup参数来确保main函数能等待所有协程完成。
-
“传球”逻辑:
- next
- 初始化: 在main函数中,cf1
- sync.WaitGroup: main函数使用了sync.WaitGroup来等待f1和f2协程完成其所有循环。wg.Add(2)表示要等待两个协程,每个协程在defer wg.Done()中调用Done()来减少计数器,wg.Wait()则会阻塞直到计数器归零。
工作原理分析
这个“传球”机制的工作流程可以概括如下:
- main函数启动f1和f2协程,并将第一个“球”发送到cf1。
- f1协程在
- f1执行其关键代码段CS1。
- f1执行next
- f2协程在
- f2执行其关键代码段CS2。
- f2执行next
- 这个循环持续进行,确保CS1和CS2严格交替执行。
优点与扩展性
- 严格同步: 该模式通过无缓冲通道的阻塞特性,确保了关键代码段的严格交替执行,不会出现竞态条件或乱序。
- 清晰的控制流: “传球”的概念直观明了,使得代码逻辑易于理解和维护。
- 良好的可扩展性: 这种模式可以轻松扩展到更多协程的交替执行。例如,如果有f1, f2, f3三个协程需要交替执行,可以创建cf1, cf2, cf3三个通道,并按f1(cf1, cf2), f2(cf2, cf3), f3(cf3, cf1)的方式连接它们,形成一个更大的循环。
注意事项
- 主协程阻塞: 在实际应用中,如果协程需要持续运行或执行多次循环,main函数必须有机制来等待这些协程完成,否则主协程可能会提前退出,导致子协










