参考: 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)を書かなくても安全に連携できるのがチャネルの強み