Pythonで並行処理を行う際、プロセス
、サブプロセス
、スレッド
という言葉をよく耳にします。しかし、それぞれの概念や用途に関しては、初心者にとっては少し混乱を招くことがあります。この記事では、これら3つの用語を明確に区別し、それぞれがどのように利用されるかについて解説します。
目次
- プロセス(Process)とは?
- サブプロセス(Subprocess)とは?
- スレッド(Thread)とは?
- GIL(Global Interpreter Lock)とは?
- それぞれの使いどころ
- まとめ
1. プロセス(Process)とは?
プロセスは、プログラムが実行中にオペレーティングシステム(OS)によって管理される独立した実行単位です。プロセスは、独自のメモリ空間を持ち、他のプロセスと直接的なメモリ共有を行いません。Pythonでは、multiprocessing
モジュールを使用してプロセスを扱うことができます。
特徴
- 独立したメモリ空間: 各プロセスは独立して動作するため、1つのプロセスがクラッシュしても他のプロセスには影響を与えません。
- マルチコア利用: 複数のプロセスを並行して実行することで、マルチコアCPUを効果的に活用できます。
- 並列処理: 複数のCPUを使って実行されるため、CPUバウンドな処理に有利です。
使いどころ
- CPU集中的な処理(計算処理やデータ処理など)を並列で行いたい場合。
- PythonのGlobal Interpreter Lock(GIL)を回避したい場合(後述のスレッドの章で説明)。
使用例
from multiprocessing import Process
def worker(num):
print(f"Worker {num} is working")
if __name__ == "__main__":
processes = []
for i in range(4):
p = Process(target=worker, args=(i,))
processes.append(p)
p.start()
for p in processes:
p.join()
2. サブプロセス(Subprocess)とは?
サブプロセスは、Pythonから外部のプログラムやシェルコマンドを実行するための方法です。subprocess
モジュールを使うことで、外部コマンドを実行し、その結果を受け取ったり、標準入力/出力を操作することができます。サブプロセスはあくまで外部プログラムを実行するために使われるものであり、内部で複数のスレッドやプロセスを扱うわけではありません。
特徴
- 外部プログラムの実行: Pythonプログラムからシェルコマンドや別のプログラムを実行します。
- 標準入出力の操作: 外部プログラムの標準入力、標準出力、標準エラーを制御できます。
- 非同期実行も可能: サブプロセスを非同期で実行し、後で結果を取得できます。
使いどころ
- 外部のコマンドやプログラムを実行する場合(例えば、シェルスクリプトや別のプログラムをPythonから呼び出したい時)。
- Python外で動作するプロセスを管理したい時。
使用例
import subprocess
# シェルコマンドを実行
result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
print(result.stdout)
3. スレッド(Thread)とは?
スレッドは、プロセス内で並行して実行される最小単位の実行単位です。スレッドは同じメモリ空間を共有しながら実行されるため、プロセス間の通信は比較的高速です。Pythonでは、threading
モジュールを使用してスレッドを管理します。
特徴
- 共有メモリ空間: スレッドは同じプロセス内で動作するため、メモリ空間を共有します。そのため、データのやり取りが比較的高速です。
- 軽量: プロセスに比べてスレッドはリソース消費が少なく、より軽量な並行処理が可能です。
- Global Interpreter Lock(GIL): PythonにはGILが存在するため、スレッドはマルチコアCPUでの並列処理に制限があります。特にCPUバウンドな処理を行う場合、スレッドは効果が薄くなります。
使いどころ
- I/O集中的な処理(ネットワーク通信やファイル操作など)の並行処理。
- 軽量な並行処理が必要な場合。
- CPUバウンドではない処理に対してスレッドを使う。
使用例
import threading
def worker(num):
print(f"Worker {num} is working")
threads = []
for i in range(4):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
4. GIL(Global Interpreter Lock)とは?
GIL(Global Interpreter Lock)は、Pythonの実行環境であるCPython(Pythonの標準実装)に存在するロック機構で、同時に1つのスレッドしかPythonのコードを実行できないという制約を課します。この制約は、Pythonスレッドが複数あっても、実際に同時に動作するスレッドは1つだけであることを意味します。
具体的には、スレッドがCPUバウンドな処理を行っている場合、GILによって他のスレッドがそのコードを実行することを制限します。これにより、複数のスレッドが同時に実行されることがなく、CPUをフル活用することができません。別の言い方をすれば、CPUバウンドな処理を並行して行うためにスレッドを増やしても、実行速度の向上は期待できないということです。
GILの影響
- CPUバウンドな処理には不向き: GILの存在により、CPUを多く消費する処理(例: 数値計算やアルゴリズムの処理など)を複数のスレッドで並行実行しても、パフォーマンス向上には繋がりません。1つのスレッドがGILを保持している間、他のスレッドは待機するためです。
- I/Oバウンドな処理には有効: GILは、スレッドがI/Oを待機している間は解放されるため、ネットワーク通信やファイル操作など、I/Oバウンドな処理を並行して行う場合は、スレッドを使うことで効率的な並行処理が可能です。
GILの回避方法
-
マルチプロセス: GILはプロセスごとに独立しているため、
multiprocessing
モジュールを使って複数のプロセスで並列処理を行うことができます。これにより、マルチコアCPUをフル活用できます。 - 外部ライブラリの利用: 数値計算や科学計算で広く使用されるライブラリ(例: NumPy、TensorFlow)は、C言語で実装されており、GILを解放して並列処理を行います。
5. それぞれの使いどころ
特徴 | プロセス | サブプロセス | スレッド |
---|---|---|---|
メモリ空間 | 独立 | 独立 | 共有 |
並行処理 | 並列処理 | 外部プログラム実行 | 同時実行 |
使用場面 | CPU集中的な処理 | 外部コマンド実行 | I/O集中的な処理 |
マルチコア利用 | 可能 | なし |
| GILによる制限 |
- プロセスは、計算処理やCPUバウンドなタスクに最適です。特に、マルチコアを活用して並列処理を行いたい場合に有効です。
- サブプロセスは、外部プログラムやシェルコマンドを実行する場面に適しています。Python内で別のプログラムを動かしたり、その出力を扱ったりする場合に使用します。
- スレッドは、メモリを共有して並行処理を行うため、I/Oバウンドなタスクに適していますが、PythonのGILによりCPUバウンドなタスクには不向きです。
6. まとめ
- プロセスは、独立したメモリ空間を持つため、CPUバウンドな処理で並列実行を行いたい時に使います。
- サブプロセスは、Pythonから外部プログラムやシェルコマンドを実行するために使用します。
- スレッドは、メモリを共有して並行処理を行うため、I/Oバウンドな処理に向いていますが、GILの制約により、CPUバウンドな処理には効果が薄いです。
Pythonでプロセス・スレッドを意識してプログラムを書く事で、効率的な処理に繋がりますので、より上級者を目指すためにも、これらの内容を常に意識してプログラミングに取り組みましょう。