`make(chan _, _)` 是原子操作吗?

huangapple go评论75阅读模式
英文:

Is `make(chan _, _)` atomic?

问题

修改一个消费者正在读取的通道是否是线程安全的?

考虑以下代码:

func main(){
    channel     := make(chan int, 3)
    channel_ptr := &channel

    go supplier (channel_ptr)
    go consumer (channel_ptr)
    
    temp = *channel_ptr
    // 重要部分
    *channel_ptr = make(chan int, 5)
    
    more := true
    for more{
        select {
            case msg := <-temp:
                *channel_ptr <- msg
            default:
                more = false
        }
    }
    // 无限期阻塞主线程以保持子线程活动
    <-make(chan bool)
}

func consumer(c *chan int){
    for true{
        fmt.Println(<-(*c))
    }
}

func supplier(c *chan int){
    for i :=0; i < 5; i ++{
        (*c)<-i
    }
}

如果通道和make按照我想要的方式工作,我应该得到以下属性:

  1. 程序总是输出 0 1 2 3 4
  2. 程序不会因为尝试从未初始化的通道读取而导致恐慌(即,我标记为重要部分的部分是原子的)

从几次测试运行来看,这似乎是正确的,但我在文档中找不到相关信息,我担心会出现微妙的竞态条件。

更新

是的,我之前的做法是行不通的。这个线程可能已经被埋没了,但有人知道如何动态调整缓冲通道的大小吗?

英文:

Is it thread-safe to modify the channel that a consumer is reading from?

Consider the following code:

func main(){
    channel     := make(chan int, 3)
    channel_ptr := &amp;channel

    go supplier (channel_ptr)
    go consumer (channel_ptr)
    
    temp = *channel_ptr
    // Important bit
    *channel_ptr = make(chan int, 5)
    
    more := true
    for more{
        select {
            case msg := &lt;-temp:
                *channel_ptr &lt;- msg
            default:
                more = false
        }
    }
    // Block main indefinitely to keep the children alive
    &lt;-make(chan bool)
}

func consumer(c *chan int){
    for true{
        fmt.Println(&lt;-(*c))
    }
}

func supplier(c *chan int){
    for i :=0; i &lt; 5; i ++{
        (*c)&lt;-i
    }
}

If channels and make work the way that I want them to, I should get the following properties:

  1. The program always outputs 0 1 2 3 4
  2. The program will never panic from trying to read from a non-initialized channel (IE, the part I labelled Important bit is atomic)

From several test runs, this seems to be true, but I can't find it anywhere in the documentation and I'm worried about subtle race conditions.

Update

Yeah, what I was doing doesn't work. This thread is probably buried at this point, but does anybody know how to dynamically resize a buffered channel?

答案1

得分: 1

这段代码存在线程安全问题。

如果你使用-race标志来运行,以使用竞态检测器,你会看到这个bug:

$ run -race t.go
==================
警告:数据竞争
主goroutine写入地址0x00c420086018:
  main.main()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:14 +0x128

其他goroutine读取地址0x00c420086018:
  main.supplier()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:37 +0x51

Goroutine 6 (正在运行) 创建于:
  main.main()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:9 +0xb4
0
==================
1
2
3
==================
警告:数据竞争
其他goroutine读取地址0x00c420086018:
  main.supplier()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:37 +0x51

主goroutine写入地址0x00c420086018:
  main.main()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:14 +0x128

Goroutine 6 (正在运行) 创建于:
  main.main()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:9 +0xb4
==================
4

作为一个经验法则,你不应该将通道作为指针传递。通道在内部已经是一个指针。

稍微退后一点:我不明白你想要实现什么。

我猜你有一个原因要将通道作为指针传递。在Go中使用通道的模式是:你创建它一次,然后将它作为值传递。你不传递它的指针,也不在创建后修改它。

在你的示例中,问题在于你有一个共享的内存片段(由channel_ptr指向的内存地址),你在一个线程中写入该内存,而另一个线程读取它。这就是数据竞争。

这不仅仅适用于通道,如果它是指向int的指针,并且两个线程正在修改int的值,你也会遇到相同的问题。

英文:

It's not thread safe.

If you run with -race flag to use race detector, you'll see the bug:

$ run -race t.go
==================
WARNING: DATA RACE
Write at 0x00c420086018 by main goroutine:
  main.main()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:14 +0x128

Previous read at 0x00c420086018 by goroutine 6:
  main.supplier()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:37 +0x51

Goroutine 6 (running) created at:
  main.main()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:9 +0xb4
0
==================
1
2
3
==================
WARNING: DATA RACE
Read at 0x00c420086018 by goroutine 6:
  main.supplier()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:37 +0x51

Previous write at 0x00c420086018 by main goroutine:
  main.main()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:14 +0x128

Goroutine 6 (running) created at:
  main.main()
      /Users/kjk/src/go/src/github.com/kjk/go-cookbook/start-mysql-in-docker-go/t.go:9 +0xb4
==================
4

As a rule of thumb, you should never pass channel as a pointer. Channel already is a pointer internally.

Stepping back a bit: I don't understand what you're trying to achieve.

I guess there's a reason you're trying to pass a channel as a pointer. The pattern of using channels in Go is: you create it once and you pass it around as value. You don't pass a pointer to it and you never modify it after creation.

In your example the problem is that you have a shared piece of memory (memory address pointed to by channel_ptr) and you write to that memory in one thread while some other thread reads it. That's data race.

It's not specific to a channel, you would have the same issue if it was pointer to an int and two threads were modifying the value of an int.

huangapple
  • 本文由 发表于 2017年7月7日 23:34:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/44974701.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定