この記事を誰が、なぜ書いているのかについて、少し背景を説明します。私たちはGeonodeというプロキシ販売会社であり、このトピックには常に接しています。ウェブスクレイピングは典型的なI/Oバウンドのワークロードであり、「スクレイパーの動作が遅い」という相談は、私たちに寄せられる最も一般的な問い合わせの一つです。 そこで、理論に入る前に率直な事実をお伝えします:もしあなたのスクレイパーが「1ページずつ取得している」ために遅いのであれば、プロキシを購入しても速度は向上しません。 並行処理はコードの特性です。100個のプロキシエンドポイントがあっても、順次処理のループを実行すれば、100個のアイドル状態のエンドポイントを持つ順次処理型のスクレイパーに過ぎません。 まずは並行処理を改善してください。プロキシが解決するのは別の問題です。それは、並行処理が機能した「後」に発生する問題、つまり、1秒に50回のリクエストを突然送り始めたアドレスに対して、ターゲット側がレート制限をかけ始めるという状況です。どちらの問題も現実のものですが、これらは同じ問題ではなく、解決策も異なります。
以上を踏まえて、両者の違いについて説明します。
一文で説明する違い
このテーマに関するロブ・パイク氏の講演での定義が依然として最も明快であり、Goブログでも次のように簡潔に述べられています。
**並行性(Concurrency)**とは、「独立して実行されるプロセスの組み合わせ」のことです。
並行性とは、「(場合によっては関連する)計算の同時実行」である。
この違いは強調される点にあるため、ぜひ2回読んでほしい。並行性は構造に関するものであり、問題をどのように独立して進行できる単位に分解するかということだ。 並列処理は実行に関するものです――それらのパーツのうち、物理的に同じ瞬間にいくつが実行されるかということです。
人々が見落としがちな点:並行性は「書く」ものであり、並列処理は「マシンが実行する」ものです。並行プログラムを書いて、何も同時に実行されない単一のコア上で実行しても、それは依然として並行プログラムです。 独立したタスクを記述しておけば、ランタイムがそれらを交互に実行するからです。また、8つのコア上で実行される並行プログラムは、ランタイムとワークロードが許容すれば、並列化される可能性があります。
この非対称性こそが、この2つの言葉が互換性を持たない理由です。並行性は並列化を可能にしますが、それを保証するものではありません。一方、並行構造のない並列化は、そもそも実現不可能です。
なぜ混乱が続くのか
3つの理由があり、それらを明確にすることが役立ちます。
観察される挙動は、多くの場合同じです。 1つのコアで実行される並行プログラムも、4つのコアで実行される並列プログラムも、どちらも「複数のことが同時に起きている」ように見えます。外部からは、どちらなのか見分けることはできません。コアを追加してもパフォーマンスが向上しないときに、初めてその違いに気づくのです。
各言語の用語法に一貫性がない。 Pythonのthreadingモジュールは並行性(concurrency)を提供するが、歴史的には並列性(parallelism)は提供してこなかった。Pythonのmultiprocessingは両方を提供する。JavaScriptのasync/awaitは並行性を提供するが、自身のコードに対して並列性を提供することはない。Goのgoroutinesは、GOMAXPROCSまでの範囲で並行性と並列性を提供する。 ドキュメント上の同じ言葉でも、エコシステムによって意味が異なります。
たいていの場合、気にする必要はありませんが、ある時突然気にする必要が出てくるのです。 I/O バウンドのワークロードの場合、この区別はほぼ学術的なものに過ぎません。並行性だけで十分なパフォーマンス向上が得られるからです。 一方、CPUに制約されるワークロードでは、これが決定的な違いとなります。なぜなら、並行性だけでは何のメリットも得られないからです。問題は、人々がI/Oに制約される問題でうまくいったパターンを学び、それをCPUに制約される問題に適用してしまうことです。
各言語の具体的な実装方法
| 実行環境 | 並行処理の仕組み | コードにおける真の並列性 | 限界 |
|---|
| Python(デフォルトビルド) | threading, asyncio | multiprocessing 経由のみ | GIL によりバイトコードの実行がシリアル化される |
| Python(フリースレッドビルド) | threading, asyncio | はい、スレッドは並列に実行される | シングルスレッドのオーバーヘッド、エコシステムの成熟度 |
| Go | ゴルーチン + チャネル | はい、最大GOMAXPROCS | 共有状態には依然として同期が必要 |
| Node.js | イベントループ、async/await | worker_threads または子プロセス経由のみ | 1つのCPUバウンドなコールバックがすべてをブロック |
| Java / C# | スレッド、スレッドプール | はい | 共有可能な可変状態の複雑さ |
| Rust | async + スレッド | はい | コンパイラが最初から正しい実装を強制する |
Node.js は、並列処理を伴わない並行処理の最も明確な例です。 公式ドキュメントには次のように的確に説明されています。イベントループは、「デフォルトでは単一のJavaScriptスレッドが使用されているにもかかわらず、可能な限りシステムカーネルに処理をオフロードすることで、Node.jsがノンブロッキングI/O操作を実行できるようにする」ものです。 カーネルはマルチスレッドですが、JavaScript自体はそうではありません。ループは6つのフェーズ(タイマー、保留中のコールバック、アイドル/準備、ポーリング、チェック、コールバックの終了)を巡回し、操作が完了すると、「カーネルがNode.jsにその旨を通知し、適切なコールバックがポーリングキューに追加されて、最終的に実行されるようにします」。
この仕組みから、実用上の結果が直接導き出されます。Node.js では 1 万件の同時 HTTP リクエストも容易に処理できます。なぜなら、待機処理はカーネル側で行われるからです。一方、CPU 負荷の高い関数が 2 秒間実行されるだけでプロセス全体がフリーズしてしまいます。それを実行するスレッドが 1 つしかなく、それをプリエンプト(割り込み)できるものが何もないからです。
Goは正反対のアプローチをとっています。goroutineは数千個単位で作成しても負荷が軽く、スケジューラはそれらをOSスレッドに分散させ、GOMAXPROCSまで処理します(デフォルトでは利用可能なコア数に設定されています)。 つまり、Goは同じ構文で並行性と並列処理の両方を実現します。Pike氏がこの講演を行った理由はここにあります。両方を備えており、それゆえに混同されがちな言語において、この区別が最も重要だからです。
決定的な問い:あなたの作業はI/Oバウンドか、それともCPUバウンドか
以上の内容は、ワークロードに関する一つの問いに集約されます。これは、推測するよりも実際に測定してみる価値があります。
I/Oバウンドとは、プログラムの処理時間の大部分が、ネットワークの応答、ディスクの読み取り、データベースのクエリなどを待つことに費やされている状態を指します。 待機中、CPUはアイドル状態になります。並行処理こそが正しく、かつ十分な解決策です。なぜなら、並行処理により、現在の待機がまだ完了していない間に次の待機を開始できるからです。並列処理は本質的に何のメリットももたらしません。ネットワークを待つ8つのコアは、ネットワークを待つ1つのコアよりも速くはなりません。
CPUバウンドとは、プログラムの処理時間の大部分が計算(解析、圧縮、ハッシュ化、変換など)に費やされている状態を指します。CPUは飽和状態にあります。並行性だけでは何も変わりません。1つのコア上で2つの計算をインターリーブしても、それらを順次実行する場合と同じ総時間に加え、切り替えのオーバーヘッドが発生するだけです。 役立つのは並列処理のみであり、その効果も物理コアの数までしか及ばない。
どちらの状態にあるかを確認するには、推測するのではなく測定を行うべきだ。Linuxでは、timeを実行すれば即座に答えが得られる。実際の経過時間と、ユーザー時間およびシステム時間の合計を比較すればよい。 実時間がCPU時間を大幅に上回っている場合は、待機状態、つまりI/Oバウンドです。両者が近い場合は、計算処理中、つまりCPUバウンドです。
Webスクレイピングは、これら両方の要素が順次発生するため、有用な例となります。ページの取得はI/Oバウンドが強く、その後のHTMLの解析はCPUバウンドです。 適切なアーキテクチャでは、ページ取得には並行処理を、解析には並列処理を採用しますが、よくある間違いは、両方の段階に同じ戦略を適用してしまうことです。200件の並行取得を行い、単一スレッドのパーサーにデータを供給するスクレイパーは、高速なスクレイパーとは言えません。それは、背後でキューが詰まっているだけの高速なフェッチツールに過ぎないのです。
PythonのGILと、スレッドの自由化がもたらした変化
Pythonについては、状況が根本的に変化しており、これに関する多くの情報が現在では古くなっているため、独立したセクションを設ける価値があります。
これまでの経緯:CPythonのグローバルインタプリタロック(GIL)により、一度にPythonバイトコードを実行できるスレッドは1つだけでした。そのため、スレッドは並行性をもたらしましたが、並列性は実現できませんでした。GILはI/O操作の際などに解放されるため、スレッド化されたI/Oは問題なく動作しましたが、CPUに依存するスレッド処理ではうまくいかず、multiprocessingが回避策として用いられていました。
変更点:バージョン3.13のリリース以降、CPythonにはGILが無効化されたオプションのビルドが同梱されるようになりました。フリースレッドに関するドキュメントには、「フリースレッド実行では、利用可能なCPUコア上でスレッドを並列に実行することで、利用可能な処理能力を最大限に活用できます」と明記されています。
2025年6月16日にSteering Councilにより「Final」ステータスで承認されたPEP 779は、フリースレッディングを実験的機能から公式にサポートされる機能へと移行するための基準を定め、その移行時期をPython 3.14を目標としています。
これを利用する前に知っておくべき4つの実用的なポイント:
デフォルトのビルドではありません。 意図的に入手またはビルドする必要があります。ソースからビルドする場合は、configureオプションで--disable-gilを指定します。現在実行中の環境を確認するには、python -VVを実行して「free-threading build」と表示されるか、sys._is_gil_enabled()を実行して、GILが無効な場合にFalseが返されるかを確認してください。
シングルスレッドのコードは遅くなります。 ドキュメントによると、pyperformanceスイートでは「平均オーバーヘッドは、macOS aarch64での約1%から、x86-64 Linuxシステムでの8%の範囲」であると報告されています。 PEP 779 には、ステアリング・カウンシルがフリースレッド版 Python について「約 10~15% 遅くなる」と予想しており、フェーズ II では 15% をハードターゲットとして設定していることが記載されています。また、メモリ使用量の幾何平均で 20% の増加については、「効率的で安全なフリースレッドを実現するための代償」として容認しています。 プログラムがシングルスレッドの場合、このビルドは明らかな性能低下を招きます。
実行時にGILを再有効化することも可能です。 フリースレッド対応ビルドでは、環境変数PYTHON_GILやオプション-X gilを介してGILを有効にした状態で実行できます。これは、依存関係に不具合が生じた場合に役立ちます。
制約となるのは依存関係です。 C拡張モジュールは、フリースレッド対応を宣言するようにビルドする必要があります。エコシステムは大幅に進化しましたが、「私のマシンでは純粋なPythonで動作する」ということは、「私の科学計算スタックが動作する」ということとは異なります。
スクレイピングやAPI処理で主流となるI/Oバウンドのケースにおいては、これらの点は判断に影響しません。asyncio または標準ビルドのスレッドプールを使用すれば、並行処理がもたらすメリットをすでにすべて得ることができます。フリースレッディングが重要になるのは、フェッチ段階ではなく、パーシング段階がボトルネックとなっている場合です。
具体例:10,000件のURLを取得する場合
具体的な数値を見ると、その違いが明らかになります。4コアのマシンにおいて、各リクエストに200ミリ秒かかり、各レスポンスの解析に50ミリ秒のCPU処理時間が必要だと仮定します。
順次処理。 10,000 × 250 ms = 2,500 秒、約 42 分です。この時間の 80% は CPU がアイドル状態になります。
並列取得、順次解析。 100件のリクエストを並列で実行すると、取得にかかる実時間は約20秒に短縮されます。解析時間は変わらず、10,000 × 50 ms = 500秒です。 合計で約520秒、およそ9分です。4.8倍の改善となりました。どこに時間が費やされていたかに注目してください。フェッチは元の実行時間の80%を占めていましたが、現在は新しい実行時間の4%に過ぎません。変更を加えなかったパーシングが、現在では全体の96%を占めています。
4つのコアでフェッチを並行処理し、パーシングを並列化。 パーシング時間は約125秒に短縮。合計で約145秒、およそ2.5分。順次処理に比べて17倍の改善です。
これらの数値からは3つの教訓が得られます。
第一に、最大の改善効果はI/Oを並行処理で解決したことにあり、そのコストはほぼゼロです。追加のコアも不要で、共有状態の問題もなく、単にループの書き方を変えただけです。
第二に、支配的なボトルネックを解消すると、すぐに次のボトルネックが支配的になります。 これは、アムダールの法則の最も実用的な形です。実行時間の20%を占める段階を最適化しても、それをどれほど完全に解消したとしても、速度は25%以上向上することはありません。最適化の前には必ず測定を行い、その後も再度測定してください。答えは変わるからです。
第三に――ここが私たちの商業的利益に関わる部分なので、それに応じて慎重に検討してください――リクエストが1回から100回に増えた瞬間、システムは検知されるようになります。1つのホストに対して1秒間に500回のリクエストを行う単一のアドレスは、レート制限を受け、最終的にはブロックされます。 これは同時実行性の問題ではなく、いくらasyncioを適用しても解決しません。これは分散の問題であり、プロキシの存在意義そのものです。当社の一般家庭向けトラフィックは1GBあたり0.79ドルから、データセンター向けは1GBあたり0.14ドルからとなっており、2026年9月時点の価格ページで確認済みです。 ただし、順序に注意してください。まず並行処理を優先し、並行処理によって解決できない問題が生じた場合に初めてプロキシを活用します。逆の順序で行うと、利用することのできない帯域幅に対して料金を支払うことになってしまいます。
並行処理が逆効果になる場合
並行処理を増やしても、その効果は次第に薄れ、やがてマイナスに転じます。その転換点は、多くの人が予想するよりも早く訪れます。
接続数の制限。 オペレーティングシステムは、オープンファイルディスクリプターの数に上限を設けています。サーバーは、クライアントごとの同時接続数に上限を設けています。1台のマシンから1万件の同時リクエストが発生すると、CPUの限界に達するはるか前に、これらのいずれかの上限にぶつかってしまいます。そして、その失敗の兆候は、通常、明確なエラーではなく、分かりにくいエラーとして現れます。
メモリ。 処理中のリクエストには、それぞれバッファ、解析済みのヘッダー、および保留中の応答データが保持されています。それぞれ100 KBを保持する1万件の同時リクエストは、1ギガバイトのメモリを、ただ待機させるだけの状態にします。
コンテキストスイッチのオーバーヘッド。 OSスレッドは無料ではありません。それぞれがスタックを持ち、スケジューリングコストが発生します。これこそが、goroutineやコルーチンが存在する理由です。これらはコストが十分に低いため、数千個でも問題ありませんが、OSスレッドの場合は数千個となると現実的ではありません。
ターゲットの許容範囲。 接続の相手側にも都合があります。あるレートを超えると、並行処理を増やしてもデータではなく429や503エラーが発生し、リクエストを増やすにつれて実効スループットは低下します。 これが実環境において最も一般的な上限であり、かつ最も測定されにくいものです。なぜなら、リクエストは依然として「機能」しているように見えるからです――単にエラーを返すだけで、リトライループが忠実にそれを繰り返しているだけなのです。
実用的なアプローチは地味です。まず控えめな並行処理制限から始め、リクエストの試行回数ではなく「1秒あたりの完了リクエスト数」を測定し、スループットの向上が止まるまで制限を上げていきます。スループットは頭打ちになり、その後低下し始めます。最適値は頭打ちの地点にあり、それは通常、直感で想像されるよりもはるかに小さい数値――多くの場合、数百ではなく数十程度――です。
どちらも必要ない場合
「並行処理にしよう」というのが反射的な反応になってしまっているため、言及しておく価値がある。
処理が本当に小規模な場合。 100件のリクエストがそれぞれ200ミリ秒かかる場合、順次処理では20秒かかります。これが毎晩cronジョブで実行されるのであれば、20秒でも問題ありません。また、午前3時にエラーが発生した場合、並行処理のコードはデバッグがより困難になります。
順序が要件の一部である場合。 パイプラインによっては、項目を厳密な順序で処理しなければならないものや、各ステップが前の結果に依存するものがあります。このような場合、並行処理は単に役に立たないだけでなく、負荷がかかった時にのみ現れるバグの原因となります。
ボトルネックがまったく別の場所にある場合。 データベースへの書き込みが制約となっている場合、200の同時読み取り処理は、同じロックの前により長いキューを作り出すだけです。実際のボトルネックを修正してください。シリアルリソースの上流で並行処理を行うと、遅いプログラムが、メモリ問題を抱えた遅いプログラムへと変わってしまいます。
共有状態が複雑な場合。 共有の可変状態にアクセスする並行コードには同期化が必要であり、これを誤ると最悪の種類のバグ——断続的で、負荷に依存し、自分のマシンでは再現できないバグ——が発生します。処理速度が2倍になるだけであり、状態が複雑である場合、論理的に理解できる逐次処理のコードの方が、多くの場合、より優れた設計上の判断となります。
そして、私たちに関連するケースとして:特に制限のないサイトから1日に数百ページをスクレイピングする場合、並行処理もプロキシも必要ありません。ループ内で適切な遅延を挟んだrequests呼び出し1回が正解であり、私たちは、必要のないプランを売り込むよりも、そのことをお伝えしたいと考えています。
よくある質問
並行性と並列性の最も単純な違いは何ですか?
並行性とは、多くのことを同時に処理することであり、プログラムの記述方法における構造的な特性です。一方、並列性とは、多くのことを同時に実行することであり、プログラムの実行方法における物理的な特性です。 並行性は並列処理を可能にするものであり、それ自体で並列処理を実現するものではありません。
並行性なしに並列処理は可能ですか?
ここで論じている意味において、実用的な形では不可能です。 並列実行には、分散させるための独立した作業単位が必要であり、それらの単位を定義することが「並行性」の意味するところです。SIMD のようなハードウェアレベルの並列処理は例外です。これは、プログラム内に並行構造が一切ない状態で、単一の命令ストリームをデータに対して並列化するものです。
Python の GIL はまだ存在しますか?
はい、デフォルトのビルドでは存在します。 3.13以降、CPythonにはGILが無効化されたオプションのフリースレッド版も同梱されており、PEP 779により、そのビルドは3.14をターゲットとして公式にサポートされる方向へと進められました。デフォルトのビルドには依然としてGILが含まれているため、意図的にフリースレッド版のインタプリタをインストールしない限り、GILは存在します。
asyncはマルチスレッドと同じですか?
いいえ。非同期並行処理(async)は、明示的なawaitポイントで協調的な切り替えを行う単一のスレッドを使用するため、一度に実行されるコードは1つだけであり、切り替えは記述した箇所でのみ発生します。 一方、マルチスレッドは複数のOSスレッドを使用し、プリエンプティブ(割り込み)方式の切り替えがどこでも発生する可能性があります。非同期処理の方が理解しやすいと言えます。スレッドは、ランタイムが許容する範囲内で真の並列処理を実現できます。
同時リクエスト数はどれくらいにすべきですか?
思っているよりも少ない数です。まずは10件程度から始め、1秒あたりの完了リクエスト数を測定し、その数値が上昇しなくなるまで増やしていきます。上限は通常、お使いのマシンの処理能力ではなく、ターゲットサーバーの許容範囲によって決まります。その点を超えると、さらなる並行処理はスループットの向上ではなく、エラーの原因となります。
並行処理によってコードは高速化されますか?
何かを待機している場合に限ります。I/O バウンドの処理では、大幅なパフォーマンス向上が期待できます。一方、シングルコアでの CPU バウンドの処理では、スウィッチングのオーバーヘッドにより、並行処理によって処理速度がわずかに低下します。真の並列処理を実現するには、複数のコアと、それらを活用できるランタイム環境が必要です。
マルチプロセッシングとマルチスレッドの違いは何ですか?
スレッドは1つのプロセス内でメモリを共有するため、通信コストは低くなりますが、共有状態の管理は危険を伴います。 プロセスはそれぞれ独立したメモリを持つため、安全性は高い反面、通信コストは高くなります。デフォルトでビルドされたPythonでは、CPUバウンドな処理で真の並列性を実現するにはプロセスを使用し、I/Oバウンドな処理で並行性を実現するにはスレッドを使用します。
並行リクエストを実行するためにプロキシは必要ですか?
本質的には必要ありません。 並行処理によってアクセスが顕著になり、ターゲット側がレート制限をかけたり、アクセス元のアドレスをブロックしたりする可能性がある場合にのみ、プロキシが必要になります。クォータに余裕のあるAPIや、クロールが許可されているサイトに対しては、並行処理だけで十分です。アドレスごとに制限があるサイトに対しては、トラフィックの分散が制約となります。これは、コードの修正とは別の対策が必要です。
まとめ
この区別は覚えておく価値があります。なぜなら、「どうすればこれを速くできるか?」という漠然とした問いを、「待機しているのか、それとも計算しているのか?」という、検証可能な答えが得られる具体的な問いに変えることができるからです。
待機している場合は、並行処理が必要であり、使用している言語が提供するどのような形式であれ、それを活用すべきです。得られるパフォーマンス向上は大きく、通常は追加のハードウェアコストもかからず、あらゆる主流のランタイムで利用可能です。 もし計算を行っている場合、並行処理だけでは何の役にも立たず、真の並列処理――プロセス、ワーカースレッド、コアをまたぐゴルーチン、あるいはPythonの場合は、独自のトレードオフを考慮する必要があるフリースレッド型インタプリタなど――が必要となります。
実際のプログラムのほとんどは、段階的にその両方の要素を含んでおり、選択そのものよりも順序付けの方が重要です。 主要なボトルネックを解消し、再度測定すると、結果が変わっていることが予想されます。ネットワークがボトルネックの80%を占めていたパイプラインも、ネットワークの問題を解決した瞬間に、96%が構文解析のボトルネックに変わります。そして、2回目の最適化は、1回目とは全く異なる作業となるのです。