go-details

メモリを共有することで通信するな。通信することでメモリを共有せよ

1 likes

Golangの箴言 > メモリを共有することで通信するな。通信することでメモリを共有せよ


結論から言うと、この格言は**「sync.Mutex を使った共有状態のロックを避け、チャネル (chan) を使ったデータの受け渡し(所有権の移動)を使え」**ということを説いています。

具体的にどういう時にどうすべきか、2つの対比で解説します。

1. 何を使わず、何を避けるべきか

  • 避けるべき手法: グローバル変数や共有ポインタに対し、sync.Mutex (ロック) を使って安全性を担保すること。
  • どういう時に避けるべきか:
    • 複数のゴルーチンが、1つの大きなデータ構造(スライスやマップなど)に同時に書き込みや読み込みを行おうとする時。
    • 処理の順番が複雑に絡み合うデータパイプラインを作る時。
  • なぜ避けるべきか: どこでロックをかけ、どこで解除するかの管理がプログラマの自己責任になり、デッドロック(お互いがロック解除を待ってフリーズする現象)や、ロックの粒度が大きすぎることによるパフォーマンス低下を引き起こしやすいからです。

2. 代わりに何を使うべきか

  • 使うべき手法: チャネル (chan) を使って、データを別のゴルーチンへ送信(メッセージパッシング)すること。
  • どういう時に使うべきか:
    • あるゴルーチンでの処理結果を、別のゴルーチンに渡して続きの処理をさせたい時。
    • 複数のワーカー(ゴルーチン)にタスクを分配し、その結果を一箇所で集計したい時。
  • なぜ使うべきか: データをチャネルに「送信」した時点で、そのデータの**「所有権」が次のゴルーチンへ移動した**とみなすことができるからです。ある瞬間においてそのデータを触るゴルーチンは常に1つだけになるため、物理的にデータの競合が発生しません。

具体的なコードの比較(結果の集計)

例えば、「複数のゴルーチンで計算を行い、その結果を1つのリスト(スライス)にまとめる」というケースで比較してみます。

❌ 避けるべきアプローチ(メモリの共有による通信)

共有のスライスをMutex(lock)で守りながら、みんなでよってたかって書き込みます。

⭕ 推奨されるアプローチ(通信によるメモリの共有)

書き込み役を「1人」に限定し、他のゴルーチンはチャネルを通して「これ追加しておいて!」とデータを投げます。

現実的な補足(必ずしもMutexが悪ではない)

Goの公式も「絶対にMutexを使うな」とは言っていません。 たとえば、**「構造体の中に持たせた単純なカウンター」「オンメモリのシンプルなキャッシュ(map)」**を守るだけであれば、チャネルを大げさに用意するより sync.Mutex を使ったほうがシンプルで高速な場合が多いです。

  • データの「流れ(フロー)」を制御したい時 👉 チャネルを使う
  • 単なる「状態」を保護したい時 👉 Mutexを使う

このような使い分けが、Go言語における現実的なベストプラクティスになります。