在一个通道上监听多个goroutine


82

我有多个goroutine试图同时在同一频道上接收。似乎最后一个在通道上开始接收的goroutine获得了值。这是语言规范中的某个地方还是未定义的行为?

c := make(chan string)
for i := 0; i < 5; i++ {
    go func(i int) {
        <-c
        c <- fmt.Sprintf("goroutine %d", i)
    }(i)
}
c <- "hi"
fmt.Println(<-c)

输出:

goroutine 4

操场上的例子

编辑:

我只是意识到它比我想的还要复杂。该消息在所有goroutine中传递。

c := make(chan string)
for i := 0; i < 5; i++ {
    go func(i int) {
        msg := <-c
        c <- fmt.Sprintf("%s, hi from %d", msg, i)
    }(i)
}
c <- "original"
fmt.Println(<-c)

输出:

original, hi from 0, hi from 1, hi from 2, hi from 3, hi from 4

操场上的例子


6
我尝试了您的最后一个片段,但(令我大为欣慰的)它只输出了original, hi from 4……
Chang Qian

1
@ChangQiantime.Sleep(time.Millisecond)在通道的发送和接收之间添加一个会带回旧的行为。
伊利亚·乔利

Answers:


75

是的,它很复杂,但是有一些经验法则可以使事情简单得多。

  • 宁可使用传递给go例程的通道的形式参数,也不要访问全局范围内的通道。您可以通过这种方式获得更多的编译器检查,以及更好的模块化。
  • 避免在特定的例行程序(包括“主要”程序)的同一通道上进行读写操作。否则,死锁风险更大。

这是应用这两个准则的程序的替代版本。此案例演示了频道上的许多作家和一位读者:

c := make(chan string)

for i := 1; i <= 5; i++ {
    go func(i int, co chan<- string) {
        for j := 1; j <= 5; j++ {
            co <- fmt.Sprintf("hi from %d.%d", i, j)
        }
    }(i, c)
}

for i := 1; i <= 25; i++ {
    fmt.Println(<-c)
}

http://play.golang.org/p/quQn7xePLw

它创建了五个写入单个通道的例程,每个例程写入五次。主例程会读取所有25条消息-您可能会注意到它们出现的顺序通常不是顺序的(即,并发是显而易见的)。

此示例演示了Go频道的功能:可能有多个作者共享一个频道;Go将自动插入邮件。

同样适用于一个频道上的一位作者和多个读者,如此处的第二个示例所示:

c := make(chan int)
var w sync.WaitGroup
w.Add(5)

for i := 1; i <= 5; i++ {
    go func(i int, ci <-chan int) {
        j := 1
        for v := range ci {
            time.Sleep(time.Millisecond)
            fmt.Printf("%d.%d got %d\n", i, j, v)
            j += 1
        }
        w.Done()
    }(i, c)
}

for i := 1; i <= 25; i++ {
    c <- i
}
close(c)
w.Wait()

第二个例子包括施加在主够程等待,否则退出及时并引起其他五个够程要提前终止(由于olov此校正)

在两个示例中,都不需要缓冲。通常,将缓冲仅视为性能增强器是一个很好的原则。如果你的程序不会死锁没有缓冲区,也不会发生死锁缓冲区是(但反之并不总是正确的)。因此,作为另一条经验法则,开始时不要缓冲,然后根据需要添加它


您不需要等待所有goroutine完成吗?
mlbright

这取决于你的意思。看一下play.golang.org例子;它们具有main一旦到达末尾即终止的功能,而与其他goroutine所做的操作无关。在上面的第一个示例中,main与其他goroutine锁定在一起,因此没有问题。因为所有的信息通过发送第二个例子也工作没有问题,c 之前close函数被调用,这种情况发生之前main够程终止。(您可能会认为close在这种情况下打电话是多余的,但这是很好的做法。)
Rick-777

1
假设您想(确定性地)在上一个示例中看到15个打印输出,则需要等待。为了证明这一点,这是一个相同的示例,但有一个时间。在Printf之前睡觉:play.golang.org/p/cEP-UBPLv6
olov

这是一个带有time.Sleep的示例,并用WaitGroup固定以等待goroutine:play.golang.org/p/ESq9he_WzS
olov

我认为一开始就忽略缓冲不是一个好的建议。如果没有缓冲,您实际上就不会编写并发代码,这不仅导致您无法死锁,而且导致发送后的下一条指令已经可以使用通道另一侧的处理结果,并且您可能会无意间(或对于新手有意地)依赖它。并且一旦您依靠立即获得结果的事实而无需特别等待,并添加了缓冲区,便有了竞争条件。
用户

24

回复较晚,但是我希望这对以后的其他人有帮助,例如长轮询,“全局”按钮,向所有人广播?

有效的Go解释了这个问题:

接收器始终阻塞,直到有数据要接收为止。

这意味着您侦听1个通道的goroutine不能超过1个,并且期望所有goroutine接收相同的值。

运行此代码示例

package main

import "fmt"

func main() {
    c := make(chan int)

    for i := 1; i <= 5; i++ {
        go func(i int) {
        for v := range c {
                fmt.Printf("count %d from goroutine #%d\n", v, i)
            }
        }(i)
    }

    for i := 1; i <= 25; i++ {
        c<-i
    }

    close(c)
}

即使有5个goroutine正在监听该频道,您也不会多次看到“ count 1”。这是因为当第一个goroutine阻塞通道时,所有其他goroutine必须排队等待。当通道被解除阻塞时,计数已经被接收并从通道中删除,因此行中的下一个goroutine将获得下一个计数值。



嗯,这很有帮助。一个好选择是为每个需要信息的Go例程创建一个通道,然后在必要时在所有通道上发送消息吗?这是我能想到的选择。
ThePartyTurtle

8

这很复杂。

另外,请参阅会发生什么GOMAXPROCS = NumCPU+1。例如,

package main

import (
    "fmt"
    "runtime"
)

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU() + 1)
    fmt.Print(runtime.GOMAXPROCS(0))
    c := make(chan string)
    for i := 0; i < 5; i++ {
        go func(i int) {
            msg := <-c
            c <- fmt.Sprintf("%s, hi from %d", msg, i)
        }(i)
    }
    c <- ", original"
    fmt.Println(<-c)
}

输出:

5, original, hi from 4

而且,看看缓冲通道会发生什么。例如,

package main

import "fmt"

func main() {
    c := make(chan string, 5+1)
    for i := 0; i < 5; i++ {
        go func(i int) {
            msg := <-c
            c <- fmt.Sprintf("%s, hi from %d", msg, i)
        }(i)
    }
    c <- "original"
    fmt.Println(<-c)
}

输出:

original

您也应该能够解释这些情况。


7

我研究了现有的解决方案,并创建了简单的广播库https://github.com/grafov/bcast

    group := bcast.NewGroup() // you created the broadcast group
    go bcast.Broadcasting(0) // the group accepts messages and broadcast it to all members

    member := group.Join() // then you join member(s) from other goroutine(s)
    member.Send("test message") // or send messages of any type to the group 

    member1 := group.Join() // then you join member(s) from other goroutine(s)
    val := member1.Recv() // and for example listen for messages

2
伟大的lib,你在那里!我也找到了github.com/asaskevich/EventBus
用户

没什么大不了的,但是也许您应该提到如何退出自述文件。
用户

那里的内存泄漏
jhvaras

:(您能解释细节@jhvaras吗?
Alexander I.Grafov

2

对于一个频道上的多个goroutine监听,是的,这是可能的。关键是消息本身,您可以定义如下消息:

package main

import (
    "fmt"
    "sync"
)

type obj struct {
    msg string
    receiver int
}

func main() {
    ch := make(chan *obj) // both block or non-block are ok
    var wg sync.WaitGroup
    receiver := 25 // specify receiver count

    sender := func() {
        o := &obj {
            msg: "hello everyone!",
            receiver: receiver,
        }
        ch <- o
    }
    recv := func(idx int) {
        defer wg.Done()
        o := <-ch
        fmt.Printf("%d received at %d\n", idx, o.receiver)
        o.receiver--
        if o.receiver > 0 {
            ch <- o // forward to others
        } else {
            fmt.Printf("last receiver: %d\n", idx)
        }
    }

    go sender()
    for i:=0; i<reciever; i++ {
        wg.Add(1)
        go recv(i)
    }

    wg.Wait()
}

输出是随机的:

5 received at 25
24 received at 24
6 received at 23
7 received at 22
8 received at 21
9 received at 20
10 received at 19
11 received at 18
12 received at 17
13 received at 16
14 received at 15
15 received at 14
16 received at 13
17 received at 12
18 received at 11
19 received at 10
20 received at 9
21 received at 8
22 received at 7
23 received at 6
2 received at 5
0 received at 4
1 received at 3
3 received at 2
4 received at 1
last receiver 4
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.