web-dev-qa-db-ja.com

スレッド化モジュールとマルチプロセッシングモジュールの違いは何ですか?

Pythonのthreadingおよびmultiprocessingモジュールを使用して、特定の操作を並行して実行し、コードを高速化する方法を学習しています。

threading.Thread()オブジェクトとmultiprocessing.Process()オブジェクトの違いを理解するのは難しいかもしれません(おそらく理論的な背景がないためです)。

また、ジョブのキューをインスタンス化して(たとえば)4つだけを並行して実行し、他のジョブが実行される前にリソースが解放されるのを待つ方法を完全に明確にすることはできません。

ドキュメントの例は明確ですが、あまり網羅的ではありません。物事を少し複雑にしようとするとすぐに、多くの奇妙なエラー(ピクルスできないメソッドなど)を受け取ります。

それで、いつthreadingおよびmultiprocessingモジュールを使用すべきですか?

これら2つのモジュールの背後にある概念と、それらを複雑なタスクに適切に使用する方法を説明するリソースにリンクしていただけますか?

114
lucacerone

Giulio Francoが言ったこと は、一般的にマルチスレッド化とマルチプロセッシングに当てはまります

ただし、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:

唯一の本当の注意点は、ジョブの引数と戻り値が、プロセス間で使用可能になるようにpickle化可能でなければならないことです(pickleするのに時間がかかりすぎたりメモリを必要としません)。通常、これは問題ではありませんが、時には問題です。


しかし、ジョブが自己完結型でない場合はどうでしょうか?あるメッセージから別のメッセージにを渡すジョブの観点からコードを設計できれば、それでもかなり簡単です。プールに依存する代わりに、threading.Threadまたはmultiprocessing.Processを使用する必要がある場合があります。そして、queue.Queueまたはmultiprocessing.Queueオブジェクトを明示的に作成する必要があります。 (他にもたくさんのオプションがあります-パイプ、ソケット、群れのあるファイルなど…ですが、要点は、エグゼキュータの自動マジックが不十分な場合はsomethingを手動で行う必要があるということです。 )

しかし、メッセージの受け渡しに頼ることさえできない場合はどうでしょうか?両方が同じ構造を変更し、互いの変更を確認するために2つのジョブが必要な場合はどうなりますか?その場合、手動の同期(ロック、セマフォ、条件など)を行う必要があり、プロセスを使用する場合は、明示的な共有メモリオブジェクトを起動する必要があります。これは、マルチスレッド(またはマルチプロセッシング)が困難になるときです。それを避けることができれば、素晴らしいです。できない場合は、誰かがSO回答に入れることができる以上のものを読む必要があります。


コメントから、Pythonのスレッドとプロセスの違いを知りたいと思いました。実際、ジュリオ・フランコの答えと私のもの、そして私たちのすべてのリンクを読めば、それはすべてをカバーするはずです...しかし、要約は間違いなく役に立つので、ここに行きます:

  1. スレッドはデフォルトでデータを共有します。プロセスはしません。
  2. (1)の結果として、プロセス間でデータを送信するには、通常、酸洗いと酸洗い解除が必要です。**
  3. (1)の別の結果として、プロセス間でデータを直接共有するには、通常、データをValue、Array、およびctypesタイプなどの低レベル形式に配置する必要があります。
  4. プロセスはGILの対象ではありません。
  5. 一部のプラットフォーム(主にWindows)では、プロセスの作成と破棄に非常に費用がかかります。
  6. プロセスにはいくつかの追加の制限があり、その一部はプラットフォームによって異なります。詳細については、 プログラミングガイドライン を参照してください。
  7. threadingモジュールには、multiprocessingモジュールの機能の一部がありません。 (multiprocessing.dummyを使用して、欠落しているAPIのほとんどをスレッド上で取得することも、concurrent.futuresなどの高レベルモジュールを使用することもできます。)

*この問題を抱えているのは実際には言語であるPythonではなく、その言語の「標準」実装であるCPythonです。 Jythonなど、他の実装にはGILがありません。

**マルチプロセッシングに fork startメソッドを使用している場合(ほとんどのWindows以外のプラットフォームで可能)、各子プロセスは、子が開始されたときに親が持っていたリソースを取得します。子にデータを渡す方法。

234
abarnert

1つのプロセスに複数のスレッドが存在する場合があります。同じプロセスに属するスレッドは、同じメモリ領域を共有します(非常に同じ変数の読み取りと書き込みが可能で、互いに干渉する可能性があります)。それどころか、異なるプロセスは異なるメモリ領域に存在し、それぞれに独自の変数があります。通信するために、プロセスは他のチャネル(ファイル、パイプ、またはソケット)を使用する必要があります。

計算を並列化する場合は、おそらく同じスレッドでスレッドを連携させるため、マルチスレッドが必要になるでしょう。

パフォーマンスについて言えば、スレッドは(OSがまったく新しい仮想メモリ領域を割り当てる必要がないため)プロセスよりも作成と管理が高速であり、通常、スレッド間通信はプロセス間通信よりも高速です。ただし、スレッドのプログラミングは困難です。スレッドは互いに干渉し、互いのメモリに書き込むことができますが、これが起こる方法は常に明らかではないため(主に命令の並べ替えとメモリキャッシングのため)、アクセスを制御するために同期プリミティブが必要になりますあなたの変数に。

29
Giulio Franco

このリンク はエレガントな方法であなたの質問に答えると信じています。

簡単に言うと、サブ問題の1つが別のサブ問題の終了を待たなければならない場合、マルチスレッド化は適切です(たとえば、I/Oの重い操作の場合)。対照的に、サブ問題が実際に同時に発生する可能性がある場合は、マルチプロセッシングが推奨されます。ただし、コアの数より多くのプロセスを作成することはありません。

3
ehfaafzv

以下は、スレッドがIOにバインドされたシナリオでのマルチプロセッシングよりもパフォーマンスが高いという概念に疑問を呈するpython 2.6.xのパフォーマンスデータです。これらの結果は、40プロセッサーのIBM System x3650 M4 BDからのものです。

IOバインド処理:プロセスプールはスレッドプールよりも優れたパフォーマンスを発揮しました

>>> do_work(50, 300, 'thread','fileio')
do_work function took 455.752 ms

>>> do_work(50, 300, 'process','fileio')
do_work function took 319.279 ms

CPUバウンド処理:プロセスプールはスレッドプールよりもパフォーマンスが高い

>>> do_work(50, 2000, 'thread','square')
do_work function took 338.309 ms

>>> do_work(50, 2000, 'process','square')
do_work function took 287.488 ms

これらは厳密なテストではありませんが、マルチプロセッシングはスレッド化と比較して完全にパフォーマンスが悪いわけではないことを教えてくれます。

上記のテストのためにインタラクティブpythonコンソールで使用されるコード

from multiprocessing import Pool
from multiprocessing.pool import ThreadPool
import time
import sys
import os
from glob import glob

text_for_test = str(range(1,100000))

def fileio(i):
 try :
  os.remove(glob('./test/test-*'))
 except : 
  pass
 f=open('./test/test-'+str(i),'a')
 f.write(text_for_test)
 f.close()
 f=open('./test/test-'+str(i),'r')
 text = f.read()
 f.close()


def square(i):
 return i*i

def timing(f):
 def wrap(*args):
  time1 = time.time()
  ret = f(*args)
  time2 = time.time()
  print '%s function took %0.3f ms' % (f.func_name, (time2-time1)*1000.0)
  return ret
 return wrap

result = None

@timing
def do_work(process_count, items, process_type, method) :
 pool = None
 if process_type == 'process' :
  pool = Pool(processes=process_count)
 else :
  pool = ThreadPool(processes=process_count)
 if method == 'square' : 
  multiple_results = [pool.apply_async(square,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]
 else :
  multiple_results = [pool.apply_async(fileio,(a,)) for a in range(1,items)]
  result = [res.get()  for res in multiple_results]


do_work(50, 300, 'thread','fileio')
do_work(50, 300, 'process','fileio')

do_work(50, 2000, 'thread','square')
do_work(50, 2000, 'process','square')
2
Mario Aguilera