web-dev-qa-db-ja.com

複数のチャネルが関与している場合、selectはどのように機能しますか?

私はのような複数の非バッファリングチャネルで選択を使用するときに見つかりました

select {
case <- chana:
case <- chanb:
}

両方のチャネルにデータがある場合でも、この選択を処理するときに、ケースチャナとケースチャンブに該当する呼び出しのバランスが取れていません。

package main

import (
    "fmt"
    _ "net/http/pprof"
    "sync"
    "time"
)

func main() {
    chana := make(chan int)
    chanb := make(chan int)

    go func() {
        for i := 0; i < 1000; i++ {
            chana <- 100 * i
        }
    }()

    go func() {
        for i := 0; i < 1000; i++ {
            chanb <- i
        }
    }()

    time.Sleep(time.Microsecond * 300)

    acount := 0
    bcount := 0
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
        for {
            select {
            case <-chana:
                acount++
            case <-chanb:
                bcount++
            }
            if acount == 1000 || bcount == 1000 {
                fmt.Println("finish one acount, bcount", acount, bcount)
                break
            }
        }
        wg.Done()
    }()

    wg.Wait()
}

このデモを実行します。chana、chanbの1つが読み取り/書き込みを終了すると、他は999-1のままになる場合があります。

バランスをとる方法はありますか?

関連トピックが見つかりました
golang-channels-select-statement

9
Terry Pang

Go select ステートメントは、(ready)ケースに偏っていません。仕様からの引用:

1つ以上の通信を続行できる場合は、均一な擬似ランダム選択を介して、続行できる単一の通信が選択されます。それ以外の場合で、デフォルトのケースがある場合は、そのケースが選択されます。デフォルトのケースがない場合、「select」ステートメントは、少なくとも1つの通信が続行できるまでブロックされます。

複数の通信を続行できる場合は、ランダムに1つが選択されます。これは完全なランダム分布ではありません。仕様ではそれが保証されていませんが、ランダムです。

あなたが経験するのは、Go PlaygroundがGOMAXPROCS=1ここで確認できます )で、goroutineスケジューラがプリエンプティブではない結果です。これが意味することは、デフォルトではゴルーチンは並列実行されないということです。ブロッキング操作が発生すると(たとえば、ネットワークからの読み取り、またはブロッキングしているチャネルでの受信または送信の試行など)、実行の準備ができている別のゴルーチンが続行されると、ゴルーチンがパークに配置されます。

また、コードにはブロッキング操作がないため、ゴルーチンはパークに配置されず、「プロデューサー」ゴルーチンの1つだけが実行され、もう一方はスケジュールされない可能性があります。

GOMAXPROCS=4というローカルコンピューターでコードを実行すると、非常に「現実的な」結果が得られます。数回実行すると、出力は次のようになります。

finish one acount, bcount 1000 901
finish one acount, bcount 1000 335
finish one acount, bcount 1000 872
finish one acount, bcount 427 1000

1つのケースに優先順位を付ける必要がある場合は、この回答を確認してください: go selectステートメントの優先度を強制

selectのデフォルトの動作は、同じ優先順位を保証するものではありませんが、平均的にはそれに近いものになります。同等の優先度を保証する必要がある場合は、selectを使用しないでください。ただし、次のような2つのチャネルからの2つの非ブロッキング受信のシーケンスを実行できます。

for {
    select {
    case <-chana:
        acount++
    default:
    }
    select {
    case <-chanb:
        bcount++
    default:
    }
    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

上記の2つの非ブロッキング受信は、両方の供給値の場合、2つのチャネルを同じ速度で(同じ優先度で)ドレインします。一方が供給しない場合、他方は遅延またはブロックされることなく常に受信されます。

これについて1つ注意する点は、チャネルのnoneが受信する値を提供する場合、これは基本的に「ビジー」ループになるため、計算能力を消費することです。これを回避するために、準備ができているチャネルがないことを検出し、その後両方の受信でselectステートメントを使用します。それらのうち、CPUリソースを浪費することなく受信する準備ができています。

for {
    received := 0
    select {
    case <-chana:
        acount++
        received++
    default:
    }
    select {
    case <-chanb:
        bcount++
        received++
    default:
    }

    if received == 0 {
        select {
        case <-chana:
            acount++
        case <-chanb:
            bcount++
        }
    }

    if acount == 1000 || bcount == 1000 {
        fmt.Println("finish one acount, bcount", acount, bcount)
        break
    }
}

Goroutineスケジューリングの詳細については、次の質問を参照してください。

Goランタイムで使用されるスレッドの数

Goroutines 8kbおよびWindows OSスレッド1 mb

golangでのファイルの書き込みで多数のゴルーチンがブロックされていると、多くのスレッドが作成されないのはなぜですか?

12
icza

コメントで述べたように、バランスを確保したい場合は、読み取りゴルーチンでselectを完全に使用せずに、バッファリングされていないチャネルによって提供される同期に依存することができます。

go func() {
    for {
        <-chana
        acount++
        <-chanb
        bcount++

        if acount == 1000 || bcount == 1000 {
            fmt.Println("finish one acount, bcount", acount, bcount)
            break
        }
    }
    wg.Done()
}()
2
Matt Harrison

編集済み:供給側からもバランスをとることができますが、@ iczaの答えはこれよりも私にとってより良いオプションであるように思われ、そもそもこれを引き起こしていたスケジューリングについても説明しています。驚いたことに、それは私の(仮想)マシン上でも一方的なものでした。

これは、供給側から2つのルーチンのバランスをとることができるものです(どういうわけかPlaygroundでは動作しないようです)。

_package main

import (
    "fmt"
    _ "net/http/pprof"
    "sync"
    "sync/atomic"
    "time"
)

func main() {
    chana := make(chan int)
    chanb := make(chan int)
    var balanceSwitch int32

    go func() {
        for i := 0; i < 1000; i++ {
            for atomic.LoadInt32(&balanceSwitch) != 0 {
                fmt.Println("Holding R1")
                time.Sleep(time.Nanosecond * 1)
            }
            chana <- 100 * i
            fmt.Println("R1: Sent i", i)
            atomic.StoreInt32(&balanceSwitch, 1)

        }
    }()

    go func() {
        for i := 0; i < 1000; i++ {

            for atomic.LoadInt32(&balanceSwitch) != 1 {
                fmt.Println("Holding R2")
                time.Sleep(time.Nanosecond * 1)
            }
            chanb <- i
            fmt.Println("R2: Sent i", i)
            atomic.StoreInt32(&balanceSwitch, 0)

        }
    }()

    time.Sleep(time.Microsecond * 300)

    acount := 0
    bcount := 0
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() {
        for {
            select {
            case <-chana:
                acount++
            case <-chanb:
                bcount++
            }
            fmt.Println("Acount Bcount", acount, bcount)
            if acount == 1000 || bcount == 1000 {
                fmt.Println("finish one acount, bcount", acount, bcount)
                break
            }
        }
        wg.Done()
    }()

    wg.Wait()
}
_

atomic.LoadInt32(&balanceSwitch) != XXおよびatomic.StoreInt32(&balanceSwitch, X)、またはその他のメカニズムを変更することにより、任意の数のルーチンにマッピングできます。最善の方法ではないかもしれませんが、それが要件である場合は、そのようなオプションを検討する必要があります。お役に立てれば。

0
Ravi