ジュリオ・フランコの言うことは、マルチスレッディングとマルチプロセッシングの全般に当てはまります。
ただし、Python *には追加の問題があります。同じプロセス内の2つのスレッドが同時にPythonコードを実行できないようにするグローバルインタープリターロックがあります。これは、8つのコアがあり、8つのスレッドを使用するようにコードを変更した場合、800%のCPUを使用して8倍速く実行できないことを意味します。同じ100%CPUを使用し、同じ速度で実行されます。(実際には、共有データがなくてもスレッドによる余分なオーバーヘッドがあるため、実行速度は少し遅くなりますが、現時点では無視してください。)
これには例外があります。コードの重い計算が実際にはPythonでは発生しないが、numpyアプリのように適切なGIL処理を行うカスタムCコードを含むライブラリでは、スレッド化によって期待されるパフォーマンス上の利点が得られます。重い計算が、実行して待機するサブプロセスによって行われる場合も同様です。
さらに重要なのは、これが問題にならない場合があります。たとえば、ネットワークサーバーはほとんどの時間をネットワークからのパケットの読み取りに費やし、GUIアプリはほとんどの時間をユーザーイベントの待機に費やしています。ネットワークサーバーまたはGUIアプリでスレッドを使用する理由の1つは、メインスレッドがネットワークパケットまたはGUIイベントのサービスを継続するのを止めることなく、長時間実行される「バックグラウンドタスク」を実行できるようにすることです。そして、それはPythonスレッドでうまく機能します。(技術的には、これはPythonスレッドがコア並列性を提供しなくても、並行性を提供することを意味します。)
ただし、純粋なPythonでCPUにバインドされたプログラムを作成している場合、スレッドを多く使用しても通常は役に立ちません。
各プロセスには独自の個別のGILがあるため、個別のプロセスを使用してもGILにそのような問題はありません。もちろん、スレッドとプロセスのトレードオフは他の言語と同じですが、プロセス間でデータを共有するのはスレッド間よりも難しく、コストもかかります。膨大な数のプロセスを実行したり、作成して破棄したりするにはコストがかかります。ただし、GILは、CやJavaなどには当てはまらない方法で、プロセスに対するバランスに重きを置いています。そのため、CやJavaよりもPythonでマルチプロセッシングを使用する頻度が高くなります。
一方、Pythonの「バッテリーを含む」という哲学にはいくつかの朗報があります。1行の変更で、スレッドとプロセス間で切り替えられるコードを書くのは非常に簡単です。
入出力を除く他のジョブ(またはメインプログラム)と何も共有しない自己完結型の「ジョブ」に関してコードを設計する場合、concurrent.futures
ライブラリを使用して、次のようにスレッドプールの周りにコードを記述できます。
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...
これらのジョブの結果を取得して、それらを他のジョブに渡したり、実行順または完了順に待機したりすることもできます。Future
詳細については、オブジェクトに関するセクションを参照してください。
ここで、プログラムが常に100%のCPUを使用していることが判明し、スレッドを追加すると処理が遅くなるだけである場合は、GILの問題が発生しているため、プロセスに切り替える必要があります。最初の行を変更するだけです。
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
唯一の本当の注意点は、ジョブの引数と戻り値がピクル可能である必要があり(ピクルするのにあまり時間やメモリを必要としない)、クロスプロセスで使用できることです。通常、これは問題ではありませんが、時には問題になります。
しかし、あなたの仕事が自己完結できない場合はどうでしょうか?あるメッセージを別のメッセージに渡すジョブの観点からコードを設計できれば、それは非常に簡単です。プールを使用するthreading.Thread
か、multiprocessing.Process
その代わりに使用する必要がある場合があります。またqueue.Queue
、multiprocessing.Queue
オブジェクトを明示的に作成する必要があります。(他にもたくさんのオプションがあります。パイプ、ソケット、群れのあるファイルなどですが、要は、エグゼキューターの自動マジックが不十分な場合は、手動で何かを行う必要があります。)
しかし、メッセージパッシングに頼ることさえできない場合はどうでしょうか。同じ構造を変更してお互いの変更を確認するために2つのジョブが必要な場合はどうでしょうか。その場合、手動の同期(ロック、セマフォ、条件など)を実行する必要があり、プロセスを使用する場合は、明示的な共有メモリオブジェクトを起動する必要があります。これは、マルチスレッド(またはマルチプロセッシング)が困難になる場合です。あなたがそれを避けることができれば、素晴らしいです。できない場合は、誰かがSOの回答に入力できる以上の数を読む必要があります。
コメントから、Pythonのスレッドとプロセスの違いを知りたいと思いました。本当に、Giulio Francoの回答と私のリンク、およびすべてのリンクを読んだ場合、それですべてがカバーされます。
- スレッドはデフォルトでデータを共有します。プロセスは行いません。
- (1)の結果として、プロセス間でデータを送信するには、通常、酸洗いと酸洗いを行う必要があります。**
- (1)の別の結果として、プロセス間でデータを直接共有するには、一般に、データをValue、Array、typeなどの低レベルの形式にする必要があり
ctypes
ます。
- プロセスはGILの対象ではありません。
- 一部のプラットフォーム(主にWindows)では、プロセスの作成と破棄にはるかにコストがかかります。
- プロセスにはいくつかの追加の制限があり、その一部はプラットフォームによって異なります。詳細については、プログラミングのガイドラインを参照してください。
threading
モジュールはの機能のいくつか持っていないmultiprocessing
モジュールを。(使用multiprocessing.dummy
して、不足しているAPIのほとんどをスレッドの上に乗せることができます。または、concurrent.futures
気にすることなく、より高レベルのモジュールを使用することもできます。)
*この問題があるのは実際には言語であるPythonではなく、その言語の「標準」実装であるCPythonです。Jythonのように、他の一部の実装にはGILがありません。
** マルチプロセスにfork startメソッドを使用している場合(ほとんどの非Windowsプラットフォームで使用できます)、各子プロセスは、子が開始されたときに親が持っていたリソースを取得します。これは、データを子に渡すもう1つの方法です。
Thread
モジュール(_thread
python 3.xで呼び出されます)もあります。正直なところ、自分で違いを理解したことはありません...