Nチャンネルを聞くには?(動的選択ステートメント)


116

2つのゴルーチンを実行する無限ループを開始するには、以下のコードを使用できます。

メッセージを受け取った後、それは新しいgoroutineを開始し、永遠に続きます。

c1 := make(chan string)
c2 := make(chan string)

go DoStuff(c1, 5)
go DoStuff(c2, 2)

for ; true;  {
    select {
    case msg1 := <-c1:
        fmt.Println("received ", msg1)
        go DoStuff(c1, 1)
    case msg2 := <-c2:
        fmt.Println("received ", msg2)
        go DoStuff(c2, 9)
    }
}

次に、N個のゴルーチンに対して同じ動作をさせたいのですが、その場合、selectステートメントはどのように見えますか?

これは私が始めたコードビットですが、selectステートメントのコーディング方法が混乱しています

numChans := 2

//I keep the channels in this slice, and want to "loop" over them in the select statemnt
var chans = [] chan string{}

for i:=0;i<numChans;i++{
    tmp := make(chan string);
    chans = append(chans, tmp);
    go DoStuff(tmp, i + 1)

//How shall the select statment be coded for this case?  
for ; true;  {
    select {
    case msg1 := <-c1:
        fmt.Println("received ", msg1)
        go DoStuff(c1, 1)
    case msg2 := <-c2:
        fmt.Println("received ", msg2)
        go DoStuff(c2, 9)
    }
}

4
あなたが望んでいるのはチャンネル多重化だと思います。 golang.org/doc/effective_go.html#chan_of_chan 基本的に、1つのチャネルを聞いてから、メインチャネルに到達する複数の子チャネルがあります。関連するSO質問:stackoverflow.com/questions/10979608/…–
ブレンデン

回答:


152

これSelectは、reflectパッケージの関数を使用して実行できます。

func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool)

Selectは、ケースのリストで記述された選択操作を実行します。Go selectステートメントと同様に、少なくとも1つのケースが処理可能になるまでブロックし、均一な疑似ランダム選択を行ってから、そのケースを実行します。選択したケースのインデックスを返し、そのケースが受信操作の場合、受信した値と、その値がチャネル上の送信に対応するかどうかを示すブール値を返します(チャネルが閉じているために受信したゼロ値とは対照的です)。

SelectCase選択するチャネル、操作の方向、および送信操作の場合に送信する値を識別する構造体の配列を渡します。

だからあなたはこのようなことをすることができます:

cases := make([]reflect.SelectCase, len(chans))
for i, ch := range chans {
    cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
}
chosen, value, ok := reflect.Select(cases)
// ok will be true if the channel has not been closed.
ch := chans[chosen]
msg := value.String()

ここでより具体的な例を試すことができます:http : //play.golang.org/p/8zwvSk4kjx


4
そのような選択のケースの数に実際的な制限はありますか?それを超えると、パフォーマンスに深刻な影響を与えるものですか?
Maxim Vladimirsky

4
多分それは私の能力不足ですが、あなたがチャネルを通じて複雑な構造を送受信しているとき、私はこのパターンを処理するのが本当に難しいことに気づきました。Tim Allclairが言ったように、共有「集約」チャネルを渡すことは、私の場合ははるかに簡単でした。
ボラM.アルパー2017

90

これは、メッセージを共有の「集約」チャネルに「転送」するゴルーチンで各チャネルをラップすることで実現できます。例えば:

agg := make(chan string)
for _, ch := range chans {
  go func(c chan string) {
    for msg := range c {
      agg <- msg
    }
  }(ch)
}

select {
case msg <- agg:
    fmt.Println("received ", msg)
}

メッセージがどのチャネルから発信されたかを知る必要がある場合は、集約チャネルに転送する前に、追加情報を含む構造体でメッセージをラップできます。

私の(制限された)テストでは、このメソッドはリフレクトパッケージを使用して大幅に機能します。

$ go test dynamic_select_test.go -test.bench=.
...
BenchmarkReflectSelect         1    5265109013 ns/op
BenchmarkGoSelect             20      81911344 ns/op
ok      command-line-arguments  9.463s

ベンチマークコードはこちら


2
ベンチマークコードが正しくありません。ベンチマーク内でループb.Nする必要があります。そうでない場合、結果(b.N出力では、1、2000000000で除算されます)は完全に無意味になります。
Dave C

2
@DaveCありがとうございます!結論は変わりませんが、結果ははるかに健全です。
Tim Allclair、2015

1
実際、私は実際の数値を取得するために、ベンチマークコードをすばやくハッキングしました。このベンチマークにはまだ足りないものや間違っているものがあるかもしれませんが、リフレクトコードがより複雑になるのは、一連のゴルーチンを必要としないため、セットアップが高速であることです(GOMAXPROCS = 1の場合)。他のすべての場合では、単純なgoroutineマージチャネルが反射ソリューションを吹き飛ばします(約2桁)。
Dave C

2
reflect.Selectアプローチと比較して)1つの重要な欠点は、マージされる各チャネルで最低1つの値でマージバッファを実行するゴルーチンです。通常:(それが問題になることはありませんが、いくつかの特定のアプリケーションでは、それは契約ブレーカかもしれません。
デーブC

1
バッファ付きマージチャネルは問題を悪化させます。問題は、リフレクトソリューションのみが完全にバッファーされないセマンティクスを持つことができることです。私は先に進んで、私が実験しようとしているテストコードを、別の答えとして(うまくいけば)何を言おうとしているかを明確にするために投稿しました。
Dave C

22

以前の回答に関するいくつかのコメントを拡大し、より明確な比較を提供するために、ここでは、同じ入力、読み取られるチャネルのスライス、および各値を呼び出す関数を指定した、これまでに提示された両方のアプローチの例を示します。値が由来するチャネル。

アプローチには主に3つの違いがあります。

  • 複雑。部分的には読者の好みかもしれませんが、チャネルアプローチの方が慣用的でわかりやすく、読みやすいと思います。

  • パフォーマンス。私のXeon amd64システムでは、goroutines + channelsが反射ソリューションを約2桁実行します(一般に、Goでの反射はしばしば遅く、絶対に必要な場合にのみ使用する必要があります)。もちろん、結果を処理する関数または入力チャネルへの値の書き込みのいずれかに大幅な遅延がある場合、このパフォーマンスの違いは簡単に取るに足らないものになる可能性があります。

  • ブロッキング/バッファリングのセマンティクス。この重要性はユースケースに依存します。ほとんどの場合、それは問題にならないか、またはgoroutineマージソリューションのわずかな追加バッファリングがスループットに役立つ場合があります。ただし、1つのライターのみがブロック解除され、他のライターがブロック解除される前にその値が完全に処理されるというセマンティクスが必要な場合は、リフレクトソリューションでのみ実現できます。

送信チャネルの「ID」が必要ない場合、またはソースチャネルが決して閉じられない場合、どちらのアプローチも簡略化できることに注意してください。

Goroutineマージチャネル:

// Process1 calls `fn` for each value received from any of the `chans`
// channels. The arguments to `fn` are the index of the channel the
// value came from and the string value. Process1 returns once all the
// channels are closed.
func Process1(chans []<-chan string, fn func(int, string)) {
    // Setup
    type item struct {
        int    // index of which channel this came from
        string // the actual string item
    }
    merged := make(chan item)
    var wg sync.WaitGroup
    wg.Add(len(chans))
    for i, c := range chans {
        go func(i int, c <-chan string) {
            // Reads and buffers a single item from `c` before
            // we even know if we can write to `merged`.
            //
            // Go doesn't provide a way to do something like:
            //     merged <- (<-c)
            // atomically, where we delay the read from `c`
            // until we can write to `merged`. The read from
            // `c` will always happen first (blocking as
            // required) and then we block on `merged` (with
            // either the above or the below syntax making
            // no difference).
            for s := range c {
                merged <- item{i, s}
            }
            // If/when this input channel is closed we just stop
            // writing to the merged channel and via the WaitGroup
            // let it be known there is one fewer channel active.
            wg.Done()
        }(i, c)
    }
    // One extra goroutine to watch for all the merging goroutines to
    // be finished and then close the merged channel.
    go func() {
        wg.Wait()
        close(merged)
    }()

    // "select-like" loop
    for i := range merged {
        // Process each value
        fn(i.int, i.string)
    }
}

反射選択:

// Process2 is identical to Process1 except that it uses the reflect
// package to select and read from the input channels which guarantees
// there is only one value "in-flight" (i.e. when `fn` is called only
// a single send on a single channel will have succeeded, the rest will
// be blocked). It is approximately two orders of magnitude slower than
// Process1 (which is still insignificant if their is a significant
// delay between incoming values or if `fn` runs for a significant
// time).
func Process2(chans []<-chan string, fn func(int, string)) {
    // Setup
    cases := make([]reflect.SelectCase, len(chans))
    // `ids` maps the index within cases to the original `chans` index.
    ids := make([]int, len(chans))
    for i, c := range chans {
        cases[i] = reflect.SelectCase{
            Dir:  reflect.SelectRecv,
            Chan: reflect.ValueOf(c),
        }
        ids[i] = i
    }

    // Select loop
    for len(cases) > 0 {
        // A difference here from the merging goroutines is
        // that `v` is the only value "in-flight" that any of
        // the workers have sent. All other workers are blocked
        // trying to send the single value they have calculated
        // where-as the goroutine version reads/buffers a single
        // extra value from each worker.
        i, v, ok := reflect.Select(cases)
        if !ok {
            // Channel cases[i] has been closed, remove it
            // from our slice of cases and update our ids
            // mapping as well.
            cases = append(cases[:i], cases[i+1:]...)
            ids = append(ids[:i], ids[i+1:]...)
            continue
        }

        // Process each value
        fn(ids[i], v.String())
    }
}

[ Goプレイグラウンドの完全なコード。]


1
goroutines + channelsソリューションではすべてを実行できない、selectまたは実行できないことにも注意してくださいreflect.Select。goroutineはチャネルからすべてを消費するまで回転し続けるため、Process1早期に終了させる明確な方法はありません。複数のリーダーを使用している場合、問題が発生する可能性もあります。ゴルーチンは各チャネルから1つのアイテムをバッファリングするため、では発生しませんselect
James Henstridge、2015年

@JamesHenstridge、停止についての最初のメモは真実ではありません。Process1の停止は、Process2の停止とまったく同じ方法で停止します。たとえば、ゴルーチンが停止する必要があるときに閉じられる「停止」チャネルを追加します。Process1は、現在使用されているより単純なループではなくselectforループ内で2つのケースを必要としfor rangeます。Process2は別のケースを突き刺してcases、その値を特別に処理する必要がありますi
Dave C

それでも、ストップアーリーケースで使用されないチャネルから値を読み取っているという問題は解決しません。
James Henstridge、2016年

0

なぜ誰かがイベントを送信していると仮定して、このアプローチが機能しないのですか?

func main() {
    numChans := 2
    var chans = []chan string{}

    for i := 0; i < numChans; i++ {
        tmp := make(chan string)
        chans = append(chans, tmp)
    }

    for true {
        for i, c := range chans {
            select {
            case x = <-c:
                fmt.Printf("received %d \n", i)
                go DoShit(x, i)
            default: continue
            }
        }
    }
}

8
これはスピンループです。入力チャネルが値を持つのを待つ間、これは利用可能なすべてのCPUを消費します。select複数のチャネル(default句なし)の要点は、少なくとも1つが回転せずに準備ができるまで効率的に待機することです。
Dave C

0

おそらくより簡単なオプション:

チャネルの配列を使用する代わりに、別々のゴルーチンで実行されている関数にパラメーターとして1つのチャネルのみを渡して、コンシューマーゴルーチンでチャネルをリッスンしないのはなぜですか。

これにより、リスナーの1つのチャネルのみを選択して、単純な選択を行うことができ、複数のチャネルからのメッセージを集約するための新しいゴルーチンの作成を回避できますか?

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