1つのチャネルでリッスンしている複数のゴルーチン


82

同じチャネルで同時に受信しようとしている複数のゴルーチンがあります。チャネルで受信を開始する最後のゴルーチンが値を取得しているようです。これは言語仕様のどこかにありますか、それとも未定義の動作ですか?

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

遊び場での例

編集:

思ったより複雑だと気づきました。メッセージはすべてのゴルーチンに渡されます。

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)がチャネルの送信と受信の間にを追加すると、古い動作が復活します。
Ilia Choly 2018

回答:


75

はい、それは複雑ですが、物事をはるかに簡単に感じさせるべき経験則がいくつかあります。

  • グローバルスコープのチャネルにアクセスするのではなく、go-routineに渡すチャネルに正式な引数を使用することをお勧めします。この方法でより多くのコンパイラチェックを取得でき、モジュール性も向上します。
  • 特定のゴールーチン(「メイン」チャネルを含む)で同じチャネルで読み取りと書き込みの両方を行うことは避けてください。そうしないと、デッドロックがはるかに大きなリスクになります。

これらの2つのガイドラインを適用した、プログラムの代替バージョンを次に示します。このケースは、チャネル上の多くのライターと1人のリーダーを示しています。

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

これは、1つのチャネルに書き込む5つのゴールーチンを作成し、それぞれが5回書き込みます。メインのgo-routineは、25個のメッセージすべてを読み取ります。メッセージが表示される順序が連続していないことがよくあります(つまり、同時実行性が明らかです)。

この例は、Goチャネルの機能を示しています。複数のライターが1つのチャネルを共有することが可能です。Goはメッセージを自動的にインターリーブします。

ここの2番目の例に見られるように、同じことが1つのチャネル上の1つのライターと複数のリーダーに当てはまります。

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()

この2番目の例には、メインのゴルーチンに課せられた待機が含まれています。待機がないとすぐに終了し、他の5つのゴルーチンが早期に終了します(この修正についてはolovに感謝します)

どちらの例でも、バッファリングは必要ありませんでした。一般に、バッファリングをパフォーマンス向上剤としてのみ見なすのは良い原則です。あなたのプログラムがデッドロックしない場合はなしバッファ、それがデッドロックではないだろうとのいずれかのバッファ(その逆はできません常に真)。したがって、別の経験則として、バッファリングせずに開始し、必要に応じて後で追加します


すべてのゴルーチンが終了するのを待つ必要はありませんか?
mlbright 2013

それはあなたが何を意味するかによります。play.golang.orgの例をご覧ください。main他のゴルーチンが何をしているかに関係なく、最後に到達すると終了する機能があります。上記の最初の例でmainは、他のゴルーチンとのロックステップであるため、問題はありません。2番目の例も問題なく機能します。これは、関数が呼び出されるc 前にすべてのメッセージが経由で送信さcloseれ、これはゴルーチンが終了するmain行われるためです。(このclose場合、呼び出しは不要であると主張するかもしれませんが、それは良い習慣です。)
Rick-777

1
最後の例で(決定論的に)15のプリントアウトを表示したい場合は、待つ必要があります。これを示すために、同じ例を示しますが、時間があります。Printfの直前でスリープします:play.golang.org/p/cEP-UBPLv6
olov

そして、これは時間のある同じ例です。スリープし、WaitGroupで修正して、ゴルーチンを待機します。play.golang.org/ p
olov

最初はバッファリングを省略するのは良い推奨ではないと思います。バッファリングなしでは、実際には並行コードを記述しません。これにより、デッドロックが発生しないだけでなく、チャネルの反対側からの処理結果が、送信後の次の命令ですでに利用可能になります。あなたは意図せずに(または初心者の場合は意図的にイベントを)それに依存するかもしれません。そして、特別に待たずにすぐに結果が得られるという事実に依存し、バッファーを追加すると、競合状態になります。
ユーザー

24

返信が遅れますが、これが将来、ロングポーリング、「グローバル」ボタン、すべての人にブロードキャストするなど、他の人に役立つことを願っています

効果的なGoは問題を説明します:

受信者は、受信するデータができるまで常にブロックします。

つまり、1つのチャネルを複数のゴルーチンがリッスンすることはできず、すべてのゴルーチンが同じ値を受け取ることを期待できます。

このコード例を実行します。

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つある場合でも、「カウント1」は2回以上表示されません。これは、最初のゴルーチンがチャネルをブロックすると、他のすべてのゴルーチンが並んで待機する必要があるためです。チャネルのブロックが解除されると、カウントはすでに受信されてチャネルから削除されているため、行の次のゴルーチンは次のカウント値を取得します。


1
ありがとう-今、この例は理にかなっていますgithub.com/goinaction/code/blob/master/chapter6/listing20/…– user31208 2015
1

ああ、これは役に立ちました。情報を必要とする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
ユーザー

大したことではありませんが、Readmeで参加を解除する方法について言及する必要があります。
ユーザー

そこでのメモリリーク
jhvaras 2017

:(詳細を@jhvarasで説明できますか?
Alexander I.Grafov 2017年

2

1つのチャンネルで複数のゴルーチンを聞く場合は、はい、可能です。重要な点はメッセージ自体です。次のようなメッセージを定義できます。

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.