Golang中两个协程向同一通道发送消息导致耗时增加10倍的问题

Golang中两个协程向同一通道发送消息导致耗时增加10倍的问题 为什么两个goroutine通过通道发送和接收数据所花费的时间存在如此显著的差异?

golang 版本 1.18 这是我的代码

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)
}

第二个goroutine休眠100毫秒,第一个goroutine休眠1毫秒,

第一个goroutine处理消息类型:1 第二个goroutine处理消息类型:2

这是日志: 2024/08/13 15:07:48 type 1, id : 97 ← timeDelta: 375ns 2024/08/13 15:07:48 type 1, id : 98 ← timeDelta: 500ns 2024/08/13 15:07:48 type 1, id : 99 ← timeDelta: 458ns 2024/08/13 15:07:48 type 1, id : 100 ← timeDelta: 417ns

2024/08/13 15:07:48 type 2, id : 2 ← timeDelta: 2.958µs 2024/08/13 15:07:49 type 2, id : 3 ← timeDelta: 20.25µs 2024/08/13 15:07:49 type 2, id : 4 ← timeDelta: 12.583µs

2024/08/13 15:08:08 type 1, timeSum: 0001-01-01 00:00:00.000116749 +0000 UTC 2024/08/13 15:08:08 type 2, timeSum: 0001-01-01 00:00:00.001056743 +0000 UTC

但是,为什么第二个goroutine的发送操作比第一个goroutine花费的时间长好几倍?

是什么内部因素导致了这种差异?


更多关于Golang中两个协程向同一通道发送消息导致耗时增加10倍的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

感谢您的回复。我进一步简化了演示,以直接解决这个问题。我移除了工作协程,这样我们可以专注于发送操作。

更多关于Golang中两个协程向同一通道发送消息导致耗时增加10倍的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


涉及的因素很多,但我认为一个重要的因素可能是哪个 Go 协程在哪个系统线程(也就是 CPU 核心)上执行。 如果两个 Go 协程在同一个线程上执行,它们可以利用一些优化,尤其是在访问共享内存时。 由于 Go 协程是任意分配给操作系统线程的,一种可能的解释是,你的“快速”协程与持续执行代码的工作协程落在了同一个线程上,并且可能因为数据被同一个 CPU 核心访问而获得速度提升。

这个问题涉及到Go调度器和通道的内部实现机制。主要原因是通道发送操作的阻塞行为Go调度器的协作式调度

当两个goroutine同时向同一个通道发送数据时,通道的发送操作可能会阻塞,这取决于接收端的情况和通道缓冲区的状态。在你的代码中,由于没有接收goroutine,所有发送操作最终都会在通道缓冲区满后阻塞。

以下是关键原因和示例说明:

1. 通道发送的阻塞行为

package main

import (
	"fmt"
	"time"
)

func main() {
    ch := make(chan int, 3)
    
    // 快速发送goroutine
    go func() {
        for i := 0; i < 5; i++ {
            start := time.Now()
            ch <- i
            elapsed := time.Since(start)
            fmt.Printf("Fast goroutine send %d: %v\n", i, elapsed)
        }
    }()
    
    // 慢速发送goroutine
    go func() {
        for i := 100; i < 105; i++ {
            time.Sleep(100 * time.Millisecond)
            start := time.Now()
            ch <- i
            elapsed := time.Since(start)
            fmt.Printf("Slow goroutine send %d: %v\n", i, elapsed)
        }
    }()
    
    time.Sleep(1 * time.Second)
}

2. 调度器的影响

当通道缓冲区满时,发送操作会阻塞。Go调度器使用协作式调度,当一个goroutine阻塞时,调度器会切换到其他可运行的goroutine。在你的场景中:

  • 第一个goroutine(1ms间隔)频繁发送,快速填满缓冲区
  • 第二个goroutine(100ms间隔)发送时,通道可能已满,需要等待

3. 时间测量的问题

你的时间测量方式可能受到调度延迟的影响:

package main

import (
	"log"
	"runtime"
	"time"
)

func main() {
    const buffer = 1000
    msgChan := make(chan *Message, buffer)
    
    // 强制使用单个处理器,更容易重现问题
    runtime.GOMAXPROCS(1)
    
    go func() {
        for i := 1; i <= 5; i++ {
            time.Sleep(time.Millisecond)
            start := time.Now()
            msgChan <- &Message{msgID: i, msgType: 1}
            elapsed := time.Since(start)
            log.Printf("Type 1, ID %d: %v (Goroutine yield)\n", i, elapsed)
        }
    }()
    
    go func() {
        for i := 1; i <= 5; i++ {
            time.Sleep(100 * time.Millisecond)
            start := time.Now()
            msgChan <- &Message{msgID: i, msgType: 2}
            elapsed := time.Since(start)
            log.Printf("Type 2, ID %d: %v (May block on full channel)\n", i, elapsed)
        }
    }()
    
    time.Sleep(1 * time.Second)
}

4. 实际原因分析

从你的日志可以看出:

  • Type 1的总时间:11.6749毫秒(约116.749微秒/次)
  • Type 2的总时间:105.6743毫秒(约1056.743微秒/次)

差异的主要原因是:

  1. 通道缓冲区管理:当通道缓冲区接近满时,发送操作需要更多时间
  2. 调度器切换成本:慢速goroutine被唤醒时,可能需要等待快速goroutine释放CPU
  3. 内存分配和GC压力:快速goroutine创建更多对象,可能触发GC

5. 验证示例

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
    const buffer = 100
    ch := make(chan int, buffer)
    var wg sync.WaitGroup
    
    // 接收goroutine,避免通道永远满
    wg.Add(1)
    go func() {
        defer wg.Done()
        for i := 0; i < 200; i++ {
            <-ch
            time.Sleep(50 * time.Millisecond) // 模拟处理时间
        }
    }()
    
    // 两个发送goroutine
    for j := 0; j < 2; j++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for i := 0; i < 100; i++ {
                interval := time.Duration(id+1) * time.Millisecond
                time.Sleep(interval)
                
                start := time.Now()
                ch <- i
                elapsed := time.Since(start)
                
                fmt.Printf("Goroutine %d, send %d: %v\n", 
                    id, i, elapsed)
            }
        }(j)
    }
    
    wg.Wait()
}

这个问题的根本原因是通道的同步特性Go调度器的协作式调度机制。当多个goroutine竞争同一资源(通道)时,执行时间较长的操作会受到调度延迟和资源竞争的影响,导致时间测量出现显著差异。

回到顶部