同時実行とsychronizationの試み−第1部:最初のアプローチ


このシリーズの意図は、ゴラン生態系における同時性と同期について何かを教えることです.いつものように、シンプルでわかりやすい説明をしたいと思います.
我々はカウンタを作成したいと仮定します.すでに我々のニーズを満たす非常に速い実現.
func main() {
    i := 0

    for {
        i++
        fmt.Println(i)
    }
}
では、どのようにこの解決策をスケールするには?もちろん、並行した計算を使っているのですが、Goではスレッドについて話をしません.あなたは、数字を数えるより多くの仕事があると想像しなければなりません.何かかもしれませんが、結果は特定の順序でお願いします.これはこの例の挑戦的な部分です.我々は、同じGoroutinesを通して提供されるが、全く同じ出力を持ちたいです.
// current and desired state would be
0, 1, 2, 3, 4, 5, 6, 7, ...
// but when just introducing concurrency,
// we could maybe end up like this
0, 2, 1, 3, 5, 4, 6, 7, ...
私たちが2匹のGoroutinesを産出して、彼らをカウントさせるとき、我々は上記のような出力を予想するかもしれません.我々はここで注文を伝えることができないので、ゴロゴネスはどうにかそれを知っていなければなりません.

Goroutineコミュニケーション


これを行うにはさまざまな方法があります.メモリの一部(変数)を共有して、他のORoutineを待つことができます.もう1つの解決策は、他のGoroutineを直接伝えることである.
これらはもちろん2つの中心的な原則です、そして、それは詳細に彼らを説明するために現在このポストの範囲を越えて行くでしょう、しかし、私はあなたがすでに彼らを知っているかもしれないと思います.
GOとGOについて話しているので、プログラミングのパラダイムとルールを念頭に置いておきます.

Share memory by Communicating - A Golang core principle read more here


私は、上記の建築上の説明をグラフィックで要約しました.

トリッキーな部分は、2つの印刷Goroutinesの間のコミュニケーションです.順序が非常に高い優先順位を持っているので(このポストでないなら役に立たないでしょう).

解決策


我々がコミュニケーションについて話すとき、我々は常にチャンネルについて話します.
以下は既に問題を解決するための解決策です.
func main() {

    // initialize all channels
    printOdd := make(chan struct{})
    printEven := make(chan struct{})
    closer := make(chan struct{})

    // spawn Goroutine A
    go func() {
        start := 0

        // infinte looping
        for {

            // block until some data arrives from either channel
            select {
            case <-printEven:
                // simulate the calculation
                time.Sleep(time.Second)
                // print
                fmt.Println(start)
                start = start + 2

                // notify Goroutine B to print an even number now
                printOdd <- struct{}{}
            case <-closer:
                return
            }
        }
    }()

    // spawn Goroutine B
    go func() {
        start := 1

        for {
            select {
            case <-printOdd:
                time.Sleep(time.Second)
                fmt.Println(start)
                start = start + 2
                printEven <- struct{}{}

            case <-closer:
                return
            }
        }
    }()

    reader := bufio.NewReader(os.Stdin)
    fmt.Println("Press enter to cancel")
    fmt.Println("---------------------")

    // trigger the ping-pong
    printEven <- struct{}{}

    // wait for console input to quit
    reader.ReadString('\n')
    fmt.Println("finished")

    // we would like to let all other goroutines return, but in fact they starve away
    // when the main goroutine returns
    // closing this channel here is totally useless
    close(closer)
}
空のstruct型は、メモリ最適化のために使用されます.
我々は他のデータを共有したくないので、ちょうど他のGoroutineに通知します.
この解決策の危険は、我々が労働者goroutines(AとB)をきれいにすることができないということです.
メインGoroutineが他のすべてのGoroutinesを返すとき、同様に殺されます.私たちの例ではあまり重要ではありません.
しかし、我々が何かをきれいにしたいという使用例が、あります.使用中にいくつかのファイルを閉じたり、ネットワーク接続を閉じたりしてください.

クリーンアップ入門


私たちがあなたの労働者Goroutinesのためにクリーンアップを可能にしたいとき、我々は標準のライブラリsync.WaitGroupのうちの1つを使用することができました.

You may view the original documentation here: https://pkg.go.dev/sync/#WaitGroup


今、私たちは私たちの労働者Goroutinesのクリーンアップを有効にするためにwaitgroupsを追加している.
func main() {

    printOdd := make(chan struct{})
    printEven := make(chan struct{})
    closer := make(chan struct{})

    wg := sync.WaitGroup{}

    go func() {
        start := 0
        wg.Add(1)

        for {
            select {
            case <-printEven:
                time.Sleep(time.Second)
                fmt.Println(start)
                start = start + 2
                printOdd <- struct{}{}
            case <-closer:
                fmt.Println("finished odd printing")
                wg.Done()
                return
            }
        }
    }()

    go func() {
        start := 1
        wg.Add(1)

        for {
            select {
            case <-printOdd:
                time.Sleep(time.Second)
                fmt.Println(start)
                start = start + 2
                printEven <- struct{}{}

            case <-closer:
                fmt.Println("finished even printing")
                wg.Done()
                return
            }
        }
    }()

    reader := bufio.NewReader(os.Stdin)
    fmt.Println("Press enter to cancel")
    fmt.Println("---------------------")
    // trigger the ping-pong
    printEven <- struct{}{}

    reader.ReadString('\n')
    fmt.Println("finished")

    // we would like to let all other goroutines return
    close(closer)

    // panics, because a close on a channel may only be received once
    // and therefore the call to wg.Done() is only called once instead of twice
    wg.Wait()

    // output: fatal error: all goroutines are asleep - deadlock!
}
現在いくつかの疑問が生じている.なぜ、新しいチャンネル(closer)が他のものを返すように導入するでしょうか?我々は、ちょうどこれを達成するために我々の他のチャンネルを使用するかもしれません.しかし、これはフォローアップポストで議論する別のトリッキーな問題を紹介します.

Edit: I mixed up even and odd - as pointed out in the comments