ホーム プログラミング言語 golang go concurrent Go 言語のバッファリングされていないチャネル

Go 言語のバッファリングされていないチャネル

 
 
Go 言語のバッファなしチャネルとは、値を受信する前に値を保存する機能がないチャネルを指します。このタイプのチャネルでは、送信ゴルーチンと受信ゴルーチンの両方が同時に送信および受信操作を完了する準備ができている必要があります。

 

両方のゴルーチンが同時に準備ができていない場合、チャネルにより、最初に送信または受信したゴルーチンが待機をブロックします。チャネルへの送信と受信のこの対話は本質的に同期的です。これらの操作はどちらも、もう一方がなければ存在できません。

ブロックとは、何らかの理由でデータが到着せず、現在のコルーチン (スレッド) がブロックを解除する前に条件が満たされるまで待機し続けることを意味します。

同期とは、2 つ以上のコルーチン (スレッド) 間でデータ内容の一貫性を維持するためのメカニズムを指します。

わかりやすくするために、2 つの完全な例を見てみましょう。どちらの例も、バッファーなしのチャネルを使用して 2 つのゴルーチン間で同期的にデータを交換します。

【例1】テニスの試合で、2人のプレーヤーが2人のプレーヤーの間でボールを行き来します。プレーヤーは常に、ボールをキャッチするのを待つか、相手にボールを打つかの 2 つの状態のいずれかになります。以下のコードに示すように、2 つのゴルーチンを使用してテニス ゲームをシミュレートし、バッファなしのチャネルを使用してボールの往復をシミュレートできます。

 // このサンプルプログラムは、バッファのないチャネルを使って、
// 2つのゴルーチン間のテニスの試合をシミュレートする方法を示しています。
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// プログラムの終了を待つためのwg
var wg sync.WaitGroup

func init() {
    rand.Seed(time.Now().UnixNano())
}

// mainは、すべてのGoプログラムのエントリーポイントです。
func main() {
    // バッファのないチャネルを作成する
    court := make(chan int)

    // 2つのゴルーチンを待つため、カウントを2に増やす
    wg.Add(2)

    // 2人の選手を開始する
    go player("Nadal", court)
    go player("Djokovic", court)

    // サーブを開始する
    court <- 1

    // ゲームの終了を待つ
    wg.Wait()
}

// playerは、テニスを打っている選手をシミュレートします。
func player(name string, court chan int) {
    // 関数を終了する際にDoneを呼び出して、main関数に作業が完了したことを通知します。
    defer wg.Done()

    for {
        // 玉が打たれてくるのを待ちます
        ball, ok := <-court
        if !ok {
            // チャネルが閉じられた場合、私たちは勝ちました
            fmt.Printf("Player %s Won\n", name)
            return
        }

        // ランダムな数字を選んで、その数字でボールを落とすかどうかを判断します。
        n := rand.Intn(100)
        if n%13 == 0 {
            fmt.Printf("Player %s Missed\n", name)

            // チャネルを閉じて、私たちは負けましたと示します。
            close(court)
            return
        }

        // 打球数を表示し、打球数を1つ増やします。
        fmt.Printf("Player %s Hit %d\n", name, ball)
        ball++

        // ボールを相手に打つ
        court <- ball
    }
} 

このプログラムを実行すると、出力は以下のようになります。

Player Nadal Hit 1
Player Djokovic Hit 2
Player Nadal Hit 3
Player Djokovic Missed

Player Nadal Won

コードの説明は次のとおりです。

  • 22 行目では、int 型のバッファなしチャネルが作成され、ボールを打つときに 2 つのゴルーチンが相互に同期できるようになります。
  • 28 行目と 29 行目では、レースに参加する 2 つのゴルーチンを作成します。この時点で、両方のゴルーチンはボールを打つのを待ってブロックされます。
  • 32 行目で、ボールがチャネルに送信され、プログラムはゴルーチンがゲームに負けるまでゲームの実行を開始します。
  • 無限ループの for ステートメントは 43 行目にあります。このループにはゲームをプレイするプロセスが含まれます。
  • 45 行目で、ゴルーチンはチャネルからデータを受け取ります。このデータは、ボールの待機を示すために使用されます。この受信アクションは、データがチャネルに送信されるまでゴルーチンをロックします。チャネルの受信アクションが戻ったとき。
  • 46 行目では、ok フラグが false かどうかを確認します。値が false の場合、チャネルは閉じられており、ゲームは終了です。
  • 53 行目から 60 行目では、ゴルーチンがボールに当たるかどうかを決定するために乱数が生成されます。
  • 行 58 は、ゴルーチンがボールをミスした場合にチャネルを閉じます。その後、両方のゴルーチンが戻り、defer で宣言された Done が実行され、プログラムが終了します。
  • 64 行目で、ボールがヒットするとボールの値が 1 増加し、67 行目でボールが他のプレーヤーに送られるボールとしてチャンネルに戻されます。この時点で、スワップが完了するまで両方のゴルーチンがロックされます。

[例 2] 異なるモードを使用し、バッファなしのチャネルを使用し、ゴルーチン間でデータを同期して駅伝をシミュレートします。リレーレースでは、4 人のランナーが交代でトラックを走ります。 2、3、4走者は前の走者からバトンを受け取るまでスタートできません。レースで最も重要な部分は、同時にパスを渡す必要があるバトンのパスです。バトンを同期させるときは、リレーに参加する両方のランナーが同時にバトンを渡す準備ができている必要があります。コードを以下に示します。

 // このサンプルプログラムは、バッファのないチャネルを使って
// 4つのgoroutineの間でリレー競走をシミュレートする方法を示します
package main

import (
    "fmt"
    "sync"
    "time"
)

// プログラムの終了を待つためのwg
var wg sync.WaitGroup

// mainはすべてのGoプログラムのエントリーポイントです
func main() {
    // バッファのないチャネルを作成する
    baton := make(chan int)

    // 最後のランナーのためにカウントを+1する
    wg.Add(1)

    // 最初のランナーがバトンを持ちます
    go Runner(baton)

    // 競争を開始する
    baton <- 1

    // 競争が終了するのを待つ
    wg.Wait()
}

// Runnerは、リレー競走の一人のランナーをシミュレートします
func Runner(baton chan int) {
    var newRunner int

    // バトンを待っています
    runner := <-baton

    // ラップトラックを回り始めます
    fmt.Printf("Runner %d Running With Baton\n", runner)

    // 次のランナーを作成する
    if runner != 4 {
        newRunner = runner + 1
        fmt.Printf("Runner %d To The Line\n", newRunner)
        go Runner(baton)
    }

    // ラップトラックを回る
    time.Sleep(100 * time.Millisecond)

    // 競争が終了しましたか?
    if runner == 4 {
        fmt.Printf("Runner %d Finished, Race Over\n", runner)
        wg.Done()
        return
    }

    // バトンを次のランナーに渡す
    fmt.Printf("Runner %d Exchange With Runner %d\n",
        runner,
        newRunner)

    baton <- newRunner
} 

このプログラムを実行すると、出力は以下のようになります。

Runner 1 Running With Baton
Runner 1 To The Line
Runner 1 Exchange With Runner 2
Runner 2 Running With Baton
Runner 2 To The Line
Runner 2 Exchange With Runner 3
Runner 3 Running With Baton
Runner 3 To The Line
Runner 3 Exchange With Runner 4
Runner 4 Running With Baton
Runner 4 Finished, Race Over

コードの説明は次のとおりです。

  • 17 行目で、バトンを同期的に渡すために、int 型のバッファなしチャネル バトンが作成されます。
  • 20 行目では、メイン関数が最後のランナーが終了するまで待機するように、WaitGroup に 1 を追加します。
  • 23 行目は、最初のランナーがトラックに到着したことを通知するゴルーチンを作成します。
  • 26行目でランナーにバトンが渡され、レースが始まります。
  • 29 行目では、メイン関数が WaitGroup でブロックし、最後のランナーがレースを終了するのを待ちます。
  • 37 行目で、ゴルーチンはバトン チャネルで受信操作を実行し、バトンを待っていることを示します。
  • 46 行目、バトンが渡されると、ゴルーチンが 4 番目のランナーになるまで、新しいランナーが作成され、次のバトンを受け取る準備が整います。
  • 50 行目では、ランナーは 100 ミリ秒間トラックを周回します。
  • 55 行目で、4 番目の走者がレースを終了すると、Done が呼び出され、WaitGroup が 1 減らされて、ゴルーチンが戻ります。
  • 64 行目、ゴルーチンが 4 番目の走者ではない場合、既に待機している次の走者にバトンが渡されます。この時点で、ハンドオーバーが完了するまでゴルーチンはロックされます。

どちらの例でも、ゴルーチンを同期するためにバッファーなしのチャネルを使用してテニスとリレーのレースをシミュレートします。コードの流れは現実世界のこれら 2 つのアクティビティの流れとまったく同じであるため、コードは読みやすいです。

バッファなしチャネルの仕組みがわかったので、次のセクションでバッファ付きチャネルを紹介します。

 

「 Go 言語のバッファリングされていないチャネル」についてわかりやすく解説!絶対に観るべきベスト2動画

【元Javaエンジニア】Go言語の将来性・できること・メリットは?
Go言語で何ができるの?どこで使われてる?現役エンジニアが解説