Golang中两个协程意外从同一个通道获取相同值怎么办?
Golang中两个协程意外从同一个通道获取相同值怎么办? 我曾遇到一个情况:两个不同的 Go 协程从同一个缓冲通道中读取了相同的值,从而导致了竞态条件。
这种情况是否在某个地方被定义为可能发生的?
诚挚问候
指针!说得好!@klanghans 如果你将同一个指针两次发送到通道中,那么两个接收的 goroutine 可能会各自获得该指针的副本,如果两个 goroutine 同时解引用它,就会引入竞态条件。
更多关于Golang中两个协程意外从同一个通道获取相同值怎么办?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
非常感谢您的支持、想法和信息。 由于我在规范中找不到相关内容,我编写了一些测试来确认:
- 通过通道发送的一个值不能被多个Go例程获取(这是好事)
事实证明,如果你通过通道发送结构体(指针)并将数据复制到该结构体中,你必须确保事先对副本进行解引用,否则会发生竞态条件。
吸取了教训!
感谢大家的支持。 此致
klanghans
通常,一个通道只有一个接收者能获取到值。以下是我能想到的一些情况:
-
你是否关闭了通道?当你关闭一个通道时,所有接收者都会“收到”一个零值。你可以使用双值形式的通道接收操作:
value, ok := <-channel并检查ok来判断是实际收到了值,还是仅仅因为通道被关闭了。 -
你是否在代码中使用了
unsafe包来绕过保护通道的互斥锁,从而与通道进行交互? -
这真的是同一个通道吗?你是否使用了某些消息队列包,其 API 将队列暴露为 Go 通道?如果是这样,它们的实现可能类似于这样:
(你的服务器代码) -- go channel --> (他们的服务器) -- 网络 --> (他们的客户端) -- go channel --> (你的客户端代码)
如果两个 goroutine 同时 解引用它,就会引入竞态条件。
在任何没有同步操作介入的时刻。
$ cat racer.go
package main
import "time"
func main() {
i := 42
ch := make(chan *int, 2)
go func() { i := <-ch; *i++ }()
ch <- &i
time.Sleep(1 * time.Second)
go func() { i := <-ch; *i++ }()
ch <- &i
time.Sleep(100 * time.Millisecond)
}
$ go run -race racer.go
==================
WARNING: DATA RACE
Read at 0x00c000016100 by goroutine 8:
main.main.func2()
/home/petrus/racer.go:11 +0x64
Previous write at 0x00c000016100 by goroutine 6:
main.main.func1()
/home/petrus/racer.go:8 +0x7a
Goroutine 8 (running) created at:
main.main()
/home/petrus/racer.go:11 +0xe9
Goroutine 6 (finished) created at:
main.main()
/home/petrus/racer.go:8 +0x98
==================
Found 1 data race(s)
exit status 66
$
$ cat racer.go
package main
import "time"
func main() {
i := 42
ch := make(chan *int, 2)
go func() { i := <-ch; *i++ }()
go func() { i := <-ch; *i++ }()
ch <- &i
ch <- &i
time.Sleep(100 * time.Millisecond)
}
.
$ go run -race racer.go
==================
WARNING: DATA RACE
Read at 0x00c000016100 by goroutine 6:
main.main.func1()
/home/petrus/racer.go:8 +0x64
Previous write at 0x00c000016100 by goroutine 7:
main.main.func2()
/home/petrus/racer.go:9 +0x7a
Goroutine 6 (running) created at:
main.main()
/home/petrus/racer.go:8 +0x98
Goroutine 7 (finished) created at:
main.main()
/home/petrus/racer.go:9 +0xba
==================
Found 1 data race(s)
exit status 66
$
修改被多个 goroutine 同时访问的数据的程序必须序列化此类访问。
要序列化访问,请使用通道操作或其他同步原语(例如
sync和sync/atomic包中的原语)来保护数据。如果你必须阅读本文档的其余部分才能理解程序的行为,那么你过于聪明了。
这种情况确实可能发生,尤其是在使用缓冲通道且协程调度时机不特定时。当两个协程同时尝试从通道接收数据,而通道中恰好有多个可用值时,调度器可能会让它们各自获取到不同的值。但如果你的代码逻辑或调度时机导致它们读取了相同的值,通常是因为数据发送或接收的逻辑存在竞态条件。
以下是一个示例,演示了两个协程可能从同一个通道读取相同值的情况:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
ch := make(chan int, 5)
var wg sync.WaitGroup
// 发送数据到通道
for i := 1; i <= 5; i++ {
ch <- i
}
// 启动两个协程从通道读取
wg.Add(2)
go func() {
defer wg.Done()
if val, ok := <-ch; ok {
fmt.Printf("Goroutine 1 received: %d\n", val)
time.Sleep(10 * time.Millisecond) // 模拟处理延迟
}
}()
go func() {
defer wg.Done()
time.Sleep(5 * time.Millisecond) // 模拟调度延迟
if val, ok := <-ch; ok {
fmt.Printf("Goroutine 2 received: %d\n", val)
}
}()
wg.Wait()
close(ch)
}
在这个例子中,由于协程的启动和调度时机不确定,两个协程可能读取到相同的值(如果通道操作没有正确同步)。要避免这种情况,应确保通道的发送和接收操作是同步的,或者使用互斥锁等机制保护共享状态。例如,可以使用 sync.Mutex 或通过设计使每个值只被一个协程处理:
package main
import (
"fmt"
"sync"
)
func main() {
ch := make(chan int, 5)
var mu sync.Mutex
received := make(map[int]bool)
var wg sync.WaitGroup
// 发送数据
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch)
// 多个协程安全读取
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for val := range ch {
mu.Lock()
if !received[val] {
received[val] = true
fmt.Printf("Goroutine %d received: %d\n", id, val)
}
mu.Unlock()
}
}(i)
}
wg.Wait()
}
在第二个示例中,通过互斥锁和映射来跟踪已接收的值,确保每个值只被处理一次。这可以避免竞态条件,但可能会引入性能开销。根据具体场景选择合适的方法。

