⭐️Goroutines(ゴルーチン)

参考: https://go-tour-jp.appspot.com/concurrency/1

そもそも「並行処理」とは?

  • 並行処理(concurrency) = 複数のタスクを「同時に進行しているように」扱う仕組み
    • 例: 料理をしながら電話に出る。厳密には手は1つでも、鍋を火にかけて待っている間に電話に出れば、2つの作業が「同時に進んでいる」ように見える
  • 混同しやすい言葉に 並列処理(parallelism) がある。違いは以下
用語意味イメージ
並行(concurrency)複数タスクを切り替えながら進める(CPUが1コアでも可能)1人のシェフが複数の鍋を行き来する
並列(parallelism)複数タスクを物理的に同時実行(CPUが複数コア必要)複数のシェフが同時に別々の鍋を担当
  • Goは「並行処理」を書きやすくする言語で、実行時に複数コアがあれば「並列」にもなる、という関係

goroutine とは?

  • goroutine(ゴルーチン)は、Goのランタイムに管理される軽量なスレッド
    • スレッド(thread) = プログラムの中で「処理を実行する流れ(作業員)」のこと。1つのプログラムの中で複数のスレッドを走らせると、複数の作業を並行して進められる
    • では「軽量なスレッド」の「軽量」とは? → OSが管理する普通のスレッドは1本あたり数MBのメモリを消費し、生成・切り替えのコストも重い。一方 goroutine は数KB程度から始まり、Goのランタイムが独自に管理するため、数千〜数万本を同時に立てても軽い。この「立ち上げ・維持のコストが小さい」ことを指して軽量と呼ぶ
OSスレッド(重い)              goroutine(軽い)
┌──────────┐               ┌──┐┌──┐┌──┐┌──┐┌──┐
│  数MB    │               │KB││KB││KB││KB││KB│  ...何万本でもOK
└──────────┘               └──┘└──┘└──┘└──┘└──┘
     ▲                            ▲
  OSが管理                  Goのランタイムが管理
                     (少数のOSスレッドの上に多数のgoroutineを載せる)

goroutine の起動方法

  • 以下のように関数呼び出しに go を付けると、新しいgoroutineが起動する
go f(x, y, z)
  • 重要なのは、go を付けると呼び出し元は f の完了を待たずに次の行へ進むという点(=これが並行)

「同じアドレス空間で実行される=共有メモリへのアクセスは同期が必要」とは?

  • アドレス空間(address space) = そのプログラムが使えるメモリの範囲。goroutine 同士は同じアドレス空間を共有する=同じ変数を全員で触れる
  • 便利な反面、複数の goroutine が同じ変数へ同時に書き込むと壊れる。これを データ競合(data race) と呼ぶ
// ❌ 危険な例: 2つのgoroutineが同じ count を同時に増やす
count := 0
go func() { count++ }()  // 読む→+1→書く
go func() { count++ }()  // 読む→+1→書く
// 期待は 2 だが、タイミング次第で 1 になることがある
  • なぜ 1 になり得るか:
goroutine A          goroutine B
count(=0)を読む
                     count(=0)を読む   ← Aの+1がまだ書かれていない
+1して 1 を書く
                     +1して 1 を書く   ← Aの結果を上書き! → 最終 1
  • これを防ぐのが 同期(synchronization) = 「同時に触らせない」ための交通整理。手段は主に2つ
    • ミューテックス(Mutex): 変数に鍵をかけて、1人ずつ触らせる
    • チャネル(Channel): そもそも変数を共有せず、値を「受け渡す」ことで衝突を避ける(Go推奨のスタイル)

Goの格言: 「メモリを共有して通信するな。通信してメモリを共有せよ(Don’t communicate by sharing memory; share memory by communicating.)」 = 変数を直接みんなで触るより、チャネルで値を渡し合え、という思想

Channels(チャネル)

チャネルとは?

  • チャネル(Channel)型は、goroutine 同士が値を送受信するための「通り道(パイプ)」
  • チャネルオペレータ <- を使う(矢印が値の流れる向き)
ch := make(chan int)  // int を流せるチャネルを作る

ch <- v    // v をチャネル ch へ送信する(矢印の先=ch に入れる)
v := <-ch  // ch から受信した値を v へ割り当てる(矢印の先=v に取り出す)
  goroutine A                      goroutine B
 (送信側)                        (受信側)
     │                                 │
     │   ch <- 42                      │
     └──────────►[ chan int ]──────────►  v := <-ch  (v = 42)
                   通り道

チャネルってなんのために必要なの?

  • 理由1: goroutine 間で値を安全に受け渡すため
    • 前述のデータ競合を、変数を共有せず「渡す」ことで根本的に回避できる
  • 理由2: タイミングを合わせる(同期する)ため
    • チャネルはデフォルトで「受け取る相手が現れるまで送信側が待つ」性質がある。これで「片方の処理が終わるまで待つ」が自然に書ける
// 別goroutineの計算結果を受け取る例
func main() {
    ch := make(chan int)

    go func() {
        result := 1 + 2      // 別のgoroutineで重い計算をするイメージ
        ch <- result         // 結果をチャネルに送る
    }()

    v := <-ch                // 結果が来るまでここで待つ → 同期になる
    fmt.Println(v)           // 3
}
  • ポイント: <-ch の行で「計算が終わって値が届くまで待つ」ため、go で並行に走らせつつ、結果はちゃんと受け取れる。ロック(Mutex)を書かなくても安全に連携できるのがチャネルの強み