Golang中无缓冲通道输出无序问题解析
Golang中无缓冲通道输出无序问题解析 我在一个Go通道中插入了两个值,并且也能看到插入的时间。 第一个值总是先于第二个值进入。 然而,有时,第二个条目会先从无缓冲通道中被监听。
包含进程1和3在纳秒级时间戳的进入时间截图:

如我们所见,值3在第293187纳秒时首先被监听到, 然后值1才被监听到, 然而,值1是在第293313纳秒插入的,而值2是在第293314纳秒插入的。
更多关于Golang中无缓冲通道输出无序问题解析的实战教程也可以访问 https://www.itying.com/category-94-b0.html
基本上生产者和消费者是并行调用的,因为它们在不同的goroutine中。
更多关于Golang中无缓冲通道输出无序问题解析的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
你可以查看我的第一张截图, 第一个进程被发送,然后是第三个,然而第三个却比第一个先从通道中出来。
是的,我提到的切换不是上下文切换,它本质上是 Go 编译器对 goroutine 的调度。 不过我明白你的意思了,感谢解释。
我可以看看代码吗?请注意,获取时间戳并不是瞬间完成的,在获取时间和将项目发送到通道之间可能会发生很多事情。
吹毛求疵地说,仅供参考。这与Go编译器无关。调度goroutine的是Go运行时。编译器的工作在代码构建完成后就结束了。goroutine的调度是动态的,每次运行都可能不同。

监听器代码
你的截图并不能证明第一个进程在第三个进程之前发送了数据。 请再次查看我上面的解释;在向通道发送一个条目和记录日志之间,可能会发生很多事情。你的截图只证明了来自第一个进程的日志消息出现在第三个进程之前,但这并不能证明条目是以相同的顺序发送的。
问题不在于日志,而在于它们被消费的顺序。
具体来说: A 在第 1 纳秒被发送到通道,B 在第 2 纳秒被发送。 然而,B 在第 3 纳秒被取出,而 A 在第 4 纳秒才被取出。
基本上, A 在第 1 纳秒被发送到通道,B 在第 2 纳秒。 然而,B 在第 3 纳秒出来,A 在第 4 纳秒。
你怎么知道 A 在第 1 纳秒被发送到通道,而 B 在第 2 纳秒?
好的,那么您的意思是,可能存在一个像这样的执行顺序:
- 进程3被发送到通道。
- 然后Go编译器切换到另一个Go协程,在该协程上,进程1被发送到通道。
- 然后打印了进程1的日志。
- 然后Go编译器再次切换到之前的Go协程,并打印了进程3的日志?
是的,这正是我在上面回复中提到的意思(Getting unordered output for unbuffered channels - #6 by jauhararifin)
- 然后 go 编译器切换到另一个 goroutine,第一个进程被发送到通道。
实际上,如果你的 Go 程序中有多个线程,那里甚至可能不是上下文切换,默认情况下,你的 go 程序中的线程数量是你 CPU 核心数的 2 倍(我可能记错了具体的数字)。请注意,线程与 goroutine 是不同的。
是的,这就是为什么无法保证顺序的原因。 根据您上面提供的代码,我猜测生产者代码大致如下:
go func() {
....
p.logCh <- process
// 实际上,在上面的代码行之后,下面的 log.Printf 行之前,
// 另一个 goroutine 有可能在不同的 goroutine 中向 p.logCh 发送另一个 process
log.Printf("<the_timestamp> <the_process_id> sent")
....
}
因此,您的程序流程可能是这样的:
- 在第 1 纳秒,您创建了 2 个 goroutine:goroutine A 和 goroutine B
- 在第 2 纳秒,goroutine A 向通道发送了一个项目 X
- 在第 3 纳秒,goroutine B 向通道发送了一个项目 Y
- 在第 4 纳秒,goroutine B 记录日志 “4 Y ended and sent to channel”
- 在第 5 纳秒,goroutine A 记录日志 “5 X ended and sent to channel”
结果就是,您会看到 Y 的日志记录在 X 之前,但实际上 X 是在 Y 之前发送的。
解决方案是什么?您应该将向通道发送一个项目和记录该事件视为一个应该原子化(不能与其他操作重叠)的单一操作。一种方法是使用互斥锁。在向通道发送项目之前,先锁定互斥锁,然后记录事件,最后解锁互斥锁。
在Go语言中,无缓冲通道的发送和接收操作是同步的,但这并不意味着它们能保证严格的顺序性。通道操作涉及goroutine调度,而Go调度器可能以非确定性的顺序执行goroutine,尤其是在高并发场景下。即使发送操作在时间上略有先后,接收操作的顺序也可能因调度延迟而颠倒。
以下是一个示例,演示了无缓冲通道输出可能无序的情况:
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int) // 无缓冲通道
// 发送第一个值
go func() {
time.Sleep(1 * time.Nanosecond) // 模拟微小延迟
ch <- 1
fmt.Println("Sent 1")
}()
// 发送第二个值
go func() {
ch <- 2
fmt.Println("Sent 2")
}()
// 接收值
for i := 0; i < 2; i++ {
fmt.Println("Received:", <-ch)
}
time.Sleep(100 * time.Millisecond) // 等待输出完成
}
在这个例子中,尽管发送1的goroutine先启动(并带有1纳秒延迟),但输出可能显示2先被接收。这是因为goroutine的调度时机和通道操作的同步点可能导致顺序变化。无缓冲通道保证发送和接收的同步,但不跨多个goroutine保证全局顺序。如果需要严格顺序,应使用单个goroutine或带缓冲的通道配合同步机制。




