go-details

並行性は並列性ではない

0 likes

Golangの箴言 > 並行性は並列性ではない


解説

この格言(Goの生みの親であるRob Pike氏の言葉)が一番伝えたいのは、「『複数のタスクを同時にさばけるような段取り(並行性)』と、『物理的に全く同時に動くこと(並列性)』は全くの別物だよ」 ということです。

並行性と並列性

並行とは、一人で作業するけど、手隙の時に別なこともできる状態。
並列とは、複数人で作業すること。

別な仕事をできる状態でプログラムを組めば、あとは並列化はどうとでもなるので、普段から並行性のあるプログラミングをしましょうという意味合いもあります。

Goが重視しているのは「まず並行性」

Go が強く意識しているのは、 複雑な処理を、分かりやすく安全に分けて組み立てることです。 たとえば次のような処理は、Go と相性がいいです。

  • リクエストを受けながらログも処理する
  • 複数の外部 API を待ちながら別の仕事を進める
  • 入力、変換、出力を段階的なパイプラインに分ける
  • ある処理が詰まっても、全体が極端に止まりにくい構成にする

こうした問題では、「1つの巨大な処理を書く」よりも、 責務ごとに小さな処理へ分けて、やり取りしながら進めるほうが強いです。

Goに限らずプログラミングの基本ですがこれが並行性の発想です。

Goの専売特許ではない

軽い並行実行単位を持つ言語はGoだけではありません。 Java の Virtual Thread、Kotlin の coroutine、C# の async/await、Python の asyncio など、各言語にも同様の仕組みがあります(詳細は末尾の比較表を参照)。

ただ Go が際立っているのは、その軽量さと言語への組み込み具合です。 Go では goroutine と呼ばれる軽量スレッドを go キーワード一つで起動でき、channel という仕組みを使って goroutine 間でデータをやり取りします。 goroutine の初期スタックはわずか 2KB 程度で、数万・数十万単位で起動しても現実的なメモリ消費に収まります。 また、Go ランタイムが goroutine を自動で OS スレッドに割り当てるため、開発者が明示的にスレッドを管理する必要がありません。

Go のコードで見る

例1: 並行性のある構造

このコードでは、2つの処理を別々の goroutine で進めています。

  • API A を待つ処理
  • API B を待つ処理

ここで重要なのは、main が 「Aが終わるまで何もできない」 という形になっていないことです。

select を使って、どちらの channel から先に値が届いても受け取れるようにしている。 これは 並行性のある設計です。

このコードが実際に CPU 上で完全に並列実行されるかどうかは別問題でが構造としては並行的と言えます。


例2: 並列性を期待したくなる場面

次は CPU を使う重い計算を複数に分けるケースです。

このコードも goroutine を使っています。 構造としては並行です。

そして CPU コアが複数使える環境なら、これらが実際に並列に走る可能性が高いです。 この場合は、並行性のある設計が、並列実行の恩恵にもつながります。

ただし、ここでも順番は同じです。

  1. まず仕事を分ける
  2. それが実行環境によって並列に動くことがある

Go が最初に提供している価値は、やはり「分けやすさ」です。

FAQ

  • Go は可能なら勝手に並列になるか
     → はい。Go ランタイムが goroutine を自動で OS スレッドに割り当て、複数コアがあれば並列に実行します。GOMAXPROCS の既定値は論理 CPU 数なので、特別な設定なしに並列の恩恵を受けられます。
  • goroutine はいくつまで作れるか
     → 明示的な上限はなく、メモリの許す限り作れます。goroutine の初期スタックは約 2KB と非常に軽量なため、数万・数十万規模でも動作する事例があります。ただし、作りすぎると調整役(スケジューラ)の負荷が上がるため、fan-out パターンや worker pool で数を制御するのが実用的です。とはいえそもそも処理の合間が無い動画のエンコード(変換)処理や、画像のリサイズなどのCPUが全力疾走し続ける、スキ(待ち時間)がない処理を並行化しても速度向上は見込めません。
  • gooutineが得意する処理はなにか
    → Web APIへの通信(ネットワーク)、データベース(SQL)への問い合わせ、(条件付きで)ファイルの読み書き・・等々

資料

言語別のOSスレッドと仮想スレッドの使用メモリ

「OSスレッド」と「仮想スレッド相当」は、言語ごとに同じものではありません。 特にこの一覧の中で、Java は本当に "Virtual Thread" という機能名を持つ一方、Go は goroutine、Kotlin は coroutine、C# は Task/async、Python は asyncio Task、JavaScript は Promise/async で、仕組みがかなり違います。なので、1対1で同じ土俵では並べられないのですが、概算で表にまとめてみました。

言語OSスレッド側軽量実行単位側メモリ感の目安
JavaPlatform ThreadVirtual ThreadPlatform Thread は Linux/macOS 64bit で既定 1024KB。Virtual Thread はスタックをヒープ上の chunk として持ち、伸縮するので固定1MBを先払いしない。 (Oracle Docs)
Goランタイムの下では OS スレッドを使うgoroutineGo の最小スタックは 2048 bytes。新規 goroutine の開始スタックは固定ではなく、GC時に平均使用量を見て適応的に決まる。かなり軽い。 (go.dev)
KotlinJVM Threadcoroutinecoroutine は suspend を状態機械に変換して実現し、スレッドをブロックしない。公式例では 50,000 スレッドが最大 100GB、同数 coroutine が約 500MB。これは例ベースだが、桁感としてはかなり参考になる。 (Kotlin)
C#Thread / ThreadPool ThreadTask + async/awaitasync/await は追加スレッドを作らない。Task は thread より高水準の抽象。Thread は既定スタックを使い、.NET docs では 1MB 既定への言及があるが、正確には OS/実行ファイルヘッダ依存。 (Microsoft Learn)
Pythonthreading.Threadasyncio.Taskthreading は OS スレッドで、スタックサイズは 0 指定なら platform/configured default。asyncio はイベントループ上で Task を走らせ、await で切り替える。Task ごとに OS スレッドは増えない。 (Python documentation)
JavaScriptNode の Worker などは OS スレッドPromise / async / awaitJavaScript は基本 single-threaded の event loop で async を回す。Node の Worker は本物の thread で、stackSizeMb の既定は 4MB。通常の async/await は thread を1本ずつ増やす仕組みではない。 (MDN Web Docs)
Cpthread / Win32 thread標準では特になしLinux pthread は既定スタックが RLIMIT_STACK に依存。Windows thread は既定の stack reservation が 1MB。つまり基本は MB 級。 (man7.org)
C++std::thread の下は OS スレッドC++20 coroutinecoroutine は stackless。再開に必要なデータは通常のスタックとは別に保持される。なので "仮想スレッド" というより "軽量状態機械" に近い。 (en.cppreference.com)