两个Goroutine向同一Channel发送消息耗时差异显著的原因探究
Go带缓冲Channel发送耗时差异的原因分析
实验场景
使用Go 1.18开展如下实验:
- 创建缓冲大小为1000的
chan *Message - 启动两个Goroutine:
- 第一个Goroutine:每次休眠1ms后发送type1消息,循环执行100次
- 第二个Goroutine:每次休眠100ms后发送type2消息,循环执行100次
- 统计两者的消息发送耗时,结果显示type2的总发送耗时远高于type1,但预期两者耗时应相近。
实验代码如下:
package main import ( "log" "time" ) type Message struct { msgID int timeStamp int64 timeTime time.Time msgType int } func main() { const buffer = 1000 msgChan := make(chan *Message, buffer) timeSumType1 := time.Time{} timeSumType2 := time.Time{} go func() { for i := 1; i <= 100; i++ { time.Sleep(time.Millisecond) timeT1 := time.Now() msgChan <- &Message{msgID: i, msgType: 1} timeDiff := time.Now().Sub(timeT1) log.Println("type 1, id :", i, " <- timeDelta:", timeDiff) timeSumType1 = timeSumType1.Add(timeDiff) } }() go func() { for i := 1; i <= 100; i++ { time.Sleep(100 * time.Millisecond) timeT1 := time.Now() msgChan <- &Message{msgID: i, msgType: 2} timeDiff := time.Now().Sub(timeT1) log.Println("type 2, id :", i, " <- timeDelta:", timeDiff) timeSumType2 = timeSumType2.Add(timeDiff) } }() time.Sleep(time.Second * 20) log.Println("type 1, timeSum:", timeSumType1) log.Println("type 2, timeSum:", timeSumType2) }
差异原因及Go内部影响因素
1. Goroutine调度的唤醒延迟差异
- type2的Goroutine每次休眠100ms,属于长周期休眠,会被放入Go调度器的定时器堆中。当休眠到期后,调度器需要将其从堆中取出、重新绑定到M(操作系统线程),这个过程的延迟远高于短休眠(1ms)的type1 Goroutine。
- 短休眠的Goroutine会被更频繁地唤醒,调度器对这类任务的调度响应更快,而长休眠Goroutine被唤醒后,可能需要等待当前M上的其他Goroutine执行完毕,或者经历M/P的重新绑定流程,导致发送操作的前置延迟增加。
2. Channel发送的隐性锁竞争
- 即使Channel缓冲足够大不会阻塞,发送操作内部依然需要获取互斥锁来保护缓冲队列的状态。
- type1的Goroutine发送频率极高(每1ms一次),会频繁抢占和释放这个锁。type2的Goroutine每次发送时,大概率会遇到type1正在持有锁,需要等待锁释放,这会直接拉长单次发送的耗时。
3. 耗时统计的放大效应
- type1的单次发送耗时本身极短(纳秒级),即使累加100次,总耗时依然有限。
- type2的单次发送因调度延迟和锁等待,耗时会比type1高一个数量级,累加100次后,总耗时的差异会被显著放大。
4. 调度器优先级的隐性倾斜
- Go调度器会优先调度短周期、频繁执行的Goroutine,这类任务被标记为“活跃”任务,获得更多的CPU时间片。而长休眠的Goroutine属于“非活跃”任务,唤醒后需要排队等待调度,进一步增加了发送操作的耗时。
内容的提问来源于stack exchange,提问作者cheng xu
相关产品推荐
相关产品推荐

