【Go入門】Goの並行処理(3)_select構文で複数のチャネルを効率的に処理する

シェアする

こんにちは。Go入門ブログの第19回です。

前回、前々回とGoにおける並行処理について説明してきました。
【Go入門】Goの並行処理(1)_ゴルーチンとチャネル
【Go入門】Goの並行処理(2)_チャネルのクローズと範囲節によるループ

今回の記事では、複数のチャネルを効率的に処理する為の制御構文であるselectステートメントについて解説します。

selectステートメント

チャネルを使うことで、データの入出力を直列化させることができます。
直列化された処理の中で複数のチャネルを処理しようとした際に、状態に応じてチャネルがブロックされてしまうことがあります。

複数のチャネルに対する受信・送信処理において、ゴルーチンを停止させることなくコントロールするために、selectステートメントを使用します。
selectステートメントの基本構文は以下のようになります。

select {
  case [チャネルからの受送信処理1]:
    受送信処理1が成功した場合の処理
  case [チャネルからの受送信処理2]:
    受送信処理2が成功した場合の処理
  default:
    case節に記述した受送信処理がいずれも成功しなかった場合の処理
}

selectステートメントを使用することで、ブロックされうる複数のチャネルの読み込みを同時に並列で試行し、最初に読み込めたものを処理することができます。
また、default節を使用することで、いずれのチャネルも読み込めなかったときの処理を記述できます。

selectの挙動

それでは、実際にselectステートメントがどのような動作をするのか、サンプルコードで見ていきましょう。

package main

import "fmt"

func main() {
    // 3つのチャネルを生成
    ch1 := make(chan string, 1)
    ch2 := make(chan string, 1)
    ch3 := make(chan string, 1)
    // チャネル1、2に異なる値をセット
    ch1 <- "チャネル1を処理"
    ch2 <- "チャネル2を処理"

    // 各case節がランダムに処理される
    select {
    case x := <-ch1: // チャネル1からの受信
        fmt.Println(x)
    case y := <-ch2: // チャネル2からの受信
        fmt.Println(y)
    case ch3 <- "チャネル3を処理": // チャネル3への送信
        z := <-ch3
        fmt.Println(z)
    default:
        fmt.Println("受送信失敗")
    }
}

このサンプルを複数回実行してみてください。
実行するたびに、異なる出力が得られることが確認できるはずです。

処理の継続が可能であるcase節が複数存在する場合、GoのランタイムはどのCase節を実行するかをランダムに選択します。
なぜこのような仕様かというと、並行処理を制御するための機能であるためです。

もし仮に、最初に記述したcase節が常に実行される仕様だとしたら、複数のチャネルの実行準備が整っている場合に、後に記述された処理はいつまで経っても実行されないことになってしまいます。
これでは並行処理の意味がありませんね。
複数のチャネルを並行して処理するための合理的な仕様として、ランダムな処理選択が行われるという訳です。

default節の実行

なお、このケースでは、defaultが実行されることはありません。
default節はすべてのcase節の処理の継続が不可能である場合に実行されるためです。
たとえば以下のような例であれば、default節の処理が実行されます。

package main

import "fmt"

func main() {
    // 3つのチャネルを生成
    ch1 := make(chan string, 1)
    ch2 := make(chan string, 1)
    ch3 := make(chan string, 1)
    // 3つのチャネルにそれぞれ値をセット
    ch1 <- "チャネル1を処理"
    ch2 <- "チャネル2を処理"
    ch3 <- "チャネル3を処理"

    // すべてのcase節が処理できない場合はdefault節が実行される
    select {
    case ch1 <- "": // チャネル1への送信
        fmt.Println("チャネル1に値を追加")
    case ch2 <- "": // チャネル2への送信
        fmt.Println("チャネル2にデータを追加")
    case ch3 <- "": // チャネル3への送信
        fmt.Println("チャネル3にデータを追加")
    default:
        fmt.Println("受送信失敗")
    }
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
受送信失敗

このサンプルでは、いずれのcase節でもバッファサイズが1であるチャネルに対して2つめのデータを送信しようとしています。
このような処理は継続不可能であるため、default節の処理が実行されています。

selectによる処理の実例

さて、ここまでselectによるチャネルの制御について、基本的な仕様と動作を説明してきました。
最後に、実際にselectをどのようなケースで使用するか、現実的な使用例を見ていきたいと思います。

selectの用法としては、ループ処理との組み合わせが最も頻度が高いでしょう。
以下の例では、3つのチャネルで相互にデータの受け渡しを行い、selectによって非同期の並行処理を制御しています。

package main

import "fmt"

func main() {
    // 3つのチャネルを生成
    ch1 := make(chan int)
    ch2 := make(chan int)
    ch3 := make(chan int)

    // チャネル1から受信した数値を2倍してチャネル2へ送信
    go func() {
        for {
            i := <-ch1
            ch2 <- (i * 2)
        }
    }()

    // チャネル2から受信した数値に1加算してチャネル2へ送信
    go func() {
        for {
            i := <-ch2
            ch3 <- (i + 1)
        }
    }()

    n := 1
    for {
        select {
        case ch1 <- n:
            // 整数をインクリメントしチャネル1へ送信
            n++
        case i := <-ch3:
            // チャネル3から受信した数値を出力
            fmt.Println(i)
        default:
            // 数値が10を越えたら処理終了
            if n > 10 {
                return
            }
        }
    }
}

このサンプルの実行結果は以下のようになります。

$ go run main.go
3
5
7
9
11
13
15
17
19
21

2つのループが並行処理されていますが、selectを使用することによって非同期で処理されるチャネルのデータを適切にコントロールできていることが理解できるかと思います。

終わりに

前々回から3回に渡り、Goにおける並行処理について解説してきました。
チャネルのようなデータ構造は他のプログラミング言語にないものですので、理解が難しかったかもしれません。

Goは並行処理をシンプルに記述できるよう言語レベルでデザインされており、そのための主要な要素がゴルーチンチャネルselectです。
現時点で理解が難しい場合は、上記のことを記憶に留めておいてください。

並行処理、すなわち非同期処理はGoの強力なメリットですが、これらを使用しなくてもGoは充分に有用なプログラミング言語です。
非同期処理について深く理解していないと支障がでるケースはさほど多くありません。
より高度なパフォーマンスを求めて非同期処理に本格的に取り組む時に、これらの記事を振り返っていただければ幸いです。

スポンサーリンク
スポンサーリンク
スポンサーリンク

シェアする

フォローする

スポンサーリンク
スポンサーリンク