
go 的 channel 发送操作本身不是调度抢占点,但若发送阻塞(如无缓冲通道且无接收者),goroutine 会主动让出执行权;因此无法保证发送后立即执行后续代码,也不保证接收方 goroutine 紧随其后运行——必须依赖显式同步机制(如 channel 配对、waitgroup 或 mutex)来确保执行顺序。
go 的 channel 发送操作本身不是调度抢占点,但若发送阻塞(如无缓冲通道且无接收者),goroutine 会主动让出执行权;因此无法保证发送后立即执行后续代码,也不保证接收方 goroutine 紧随其后运行——必须依赖显式同步机制(如 channel 配对、waitgroup 或 mutex)来确保执行顺序。
在 Go 调度模型中,“抢占”(preemption)与“协作式让出”(cooperative yielding)并存。自 Go 1.14 起,运行时引入了基于系统信号的异步抢占机制(如长时间运行的循环会被中断),但 channel 操作本身不构成强制抢占点。关键在于:channel 发送(ch 是否阻塞:
- ✅ 阻塞发送(如向无缓冲通道发送,且当前无就绪接收者):goroutine 主动挂起,交还控制权给调度器,此时必然发生调度切换;
- ❌ 非阻塞发送(如向带缓冲通道发送,且缓冲未满;或有接收者已就绪):操作原子完成,不触发调度,后续代码紧随执行。
然而,这仍无法保证执行顺序。以下代码存在典型竞态风险:
// goroutine A ch <p>即使 GOMAXPROCS=1,也无法规避问题: </p>
- 若发送阻塞,A 挂起 → B 被唤醒 → B 执行接收及后续代码;
- 若发送非阻塞,A 连续执行完所有代码,B 可能在之后任意时刻被调度;
- 更重要的是,内存可见性无保障:A 写入的变量可能未及时对 B 可见(违反 Go 内存模型),即使逻辑上“先发生”。
✅ 正确做法是使用同步原语显式建模依赖关系:
-
通道配对(推荐):用第二个通道确认 A 已完成关键步骤
done := make(chan struct{}) // A ch sync.WaitGroup(适用于一次性协调)
Mutex + condition variable(复杂状态协同)
⚠️ 注意事项:
- 切勿依赖当前调度行为(如“函数调用必抢占”)——这是实现细节,已在 Go 1.14+ 中调整;
- Go 内存模型仅保证 happens-before 关系(由 channel 收发、互斥锁、Once 等明确定义),而非执行时序;
- 并发安全 ≠ 执行顺序可预测;需将“谁先运行”转化为“谁等待谁”的数据依赖。
总结:channel 是通信载体,不是调度控制器。要确保执行顺序,必须用同步原语声明依赖,而非假设调度器行为。











