Golang中两个协程意外从同一个通道获取相同值怎么办?

Golang中两个协程意外从同一个通道获取相同值怎么办? 我曾遇到一个情况:两个不同的 Go 协程从同一个缓冲通道中读取了相同的值,从而导致了竞态条件。

这种情况是否在某个地方被定义为可能发生的?

诚挚问候

6 回复

指针!说得好!@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
$

Go 编程语言规范

Go 内存模型

修改被多个 goroutine 同时访问的数据的程序必须序列化此类访问。

要序列化访问,请使用通道操作或其他同步原语(例如 syncsync/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()
}

在第二个示例中,通过互斥锁和映射来跟踪已接收的值,确保每个值只被处理一次。这可以避免竞态条件,但可能会引入性能开销。根据具体场景选择合适的方法。

回到顶部