GOMAXPROCS=1时Go通道发送为何不按协程创建顺序输出?
单P(GOMAXPROCS=1)场景下Go协程调度顺序问题解答
问题描述
设置runtime.GOMAXPROCS(1)后编写Go代码:创建10个foo协程尝试向无缓冲通道c发送数据,之后创建10个接收协程。但输出结果中第一个发送消息的是最后创建的协程9,而非按0到9的顺序输出。已知P>1时多线程调度会导致无序,但疑惑单P场景下为何出现此情况,附上代码及输出:
代码
import ( "fmt" "runtime" "time" ) func main() { runtime.GOMAXPROCS(1) c := make(chan struct{}) ok := make(chan int) n := 10 for i := 0; i < n; i++ { go foo(i, c, ok) } time.Sleep(time.Millisecond) for i := 0; i < n; i++ { go func() { <-c }() } time.Sleep(time.Second) close(ok) } func foo(i int, c chan struct{}, ok chan int) { for { select { case c <- struct{}{}: fmt.Printf("%d sendMsg!\n", i) case <-ok: fmt.Printf("%d closed\n", i) return } } }
输出
9 sendMsg! 0 sendMsg! 1 sendMsg! 2 sendMsg! 3 sendMsg! 4 sendMsg! 5 sendMsg! 6 sendMsg! 7 sendMsg! 8 sendMsg!
原因分析
即使设置GOMAXPROCS(1),Go协程调度器也不保证协程执行顺序与创建顺序一致,核心原因如下:
- 协程创建后的调度时机:循环中创建协程时,主协程不会立刻让出CPU,而是先完成整个循环(创建完10个协程),直到进入
time.Sleep时,调度器才开始调度等待的协程。 - 单P下的协程队列调度逻辑:单P场景下,调度器维护的可运行协程队列并非严格遵循先进先出(FIFO)。最后创建的协程9被加入队列末尾,但调度器选取下一个执行协程时,可能从队列尾部优先选择,导致后创建的协程先被调度。
- 无缓冲通道的唤醒策略:所有
foo协程启动后,因无接收方会阻塞在通道发送操作上,进入等待队列。当第一个接收协程创建后,通道会唤醒等待发送的协程,而唤醒的选择顺序不遵循协程创建顺序,刚好协程9被第一个选中执行。
简言之,Go协程调度器的设计目标是高效公平调度,本身就不保证协程执行顺序与创建顺序一致,单P场景下同样如此。
内容的提问来源于stack exchange,提问作者Aa troy
相关产品推荐
相关产品推荐

