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


141

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

threading.Thread()オブジェクトとオブジェクトの違いが何であるかを理解するために、これについて(理論的な背景がないので)難しいと感じていますmultiprocessing.Process()

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

ドキュメントの例は明確ですが、完全ではありません。少し複雑にしようとすると、多くの奇妙なエラーが表示されます(酸洗いできないメソッドなど)。

では、threadingmultiprocessingモジュールをいつ使用すればよいですか?

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


それだけではなく、Threadモジュール(_threadpython 3.xで呼び出されます)もあります。正直なところ、自分で違いを理解したことはありません...
Dunno 2013

3
@Dunno:Thread/の_threadドキュメントに明記されているように、これは「低レベルのプリミティブ」です。これを使用して、カスタム同期オブジェクトを構築したり、スレッドのツリーの結合順序を制御したりすることができます。なぜそれを使用する必要があるのか​​わからない場合は、使用せず、に固執してくださいthreading
abarnert 2013

回答:


260

ジュリオ・フランコの言うことは、マルチスレッディングとマルチプロセッシングの全般に当てはまります。

ただし、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.Queuemultiprocessing.Queueオブジェクトを明示的に作成する必要があります。(他にもたくさんのオプションがあります。パイプ、ソケット、群れのあるファイルなどですが、要は、エグゼキューターの自動マジックが不十分な場合は、手動で何かを行う必要があります。)

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


コメントから、Pythonのスレッドとプロセスの違いを知りたいと思いました。本当に、Giulio Francoの回答と私のリンク、およびすべてのリンクを読んだ場合、それですべてがカバーされます。

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

*この問題があるのは実際には言語であるPythonではなく、その言語の「標準」実装であるCPythonです。Jythonのように、他の一部の実装にはGILがありません。

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


感謝しますが、すべてを理解したかどうかはわかりません。とにかく、私は少し学習目的でそれをやろうとしているのですが、スレッドを単純に使用すると、コードの速度が半分になりました(同時に1000以上のスレッドを開始し、それぞれが外部アプリを呼び出します)。これは飽和します。 cpu、それでも速度がx​​2増加します)。私はスマートにスレッドを管理することは、本当に私のコードの速度を向上させるかもしれないと思う...
lucacerone

3
@LucaCerone:ああ、コードがほとんどの時間を外部プログラムの待機に費やしている場合は、はい、スレッド化のメリットがあります。いい視点ね。答えを編集して説明しましょう。
abarnert 2013

2
@LucaCerone:一方、あなたが理解していない部分は何ですか?あなたが始めている知識のレベルを知らなければ、良い答えを書くのは難しいです…しかし、いくつかのフィードバックがあれば、あなたや将来の読者にとっても役立つ何かを思いつくかもしれません。
abarnert 2013

3
@LucaCeroneマルチプロセッシングのPEPはこちらで読む必要があります。スレッドとマルチプロセッシングのタイミングと例を示します。
mr2ert 2013

1
@LucaCerone:メソッドがバインドされているオブジェクトに複雑な状態がない場合、酸洗いの問題に対する最も簡単な回避策は、オブジェクトを生成してそのメソッドを呼び出す愚かなラッパー関数を記述することです。それは場合(;非常に簡単である複雑な状態を持っている、あなたはおそらくそれをpickle化できるようにする必要がありpickleドキュメントがそれを説明する)、その後、最悪であなたの愚かなラッパーですdef wrapper(obj, *args): return obj.wrapper(*args)
abarnert 2013

32

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

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

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


12
これにはGILに関するいくつかの非常に重要な情報が欠落しているため、誤解を招く恐れがあります。
abarnert 2013

1
@ mr2ert:はい、それは一言で言えば非常に重要な情報です。:)しかし、それはそれよりも少し複雑です。そのため、私は別の答えを書きました。
abarnert 2013

2
@abarnertが正しいとコメントしていると思い、GILについてここで答えるのを忘れました。したがって、この答えは間違っています。あなたはそれを賛成しないでください。
Giulio Franco

6
Python threadingとの違いはまだまったく返されないので、私はこの回答に反対しましたmultiprocessing
Antti Haapala 2016

すべてのプロセスにGILがあることを読みました。しかし、すべてのプロセスが同じpythonインタープリターを使用していますか、それともスレッドごとに別個のインタープリターがありますか?
可変

3

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

簡単に言うと、サブ問題の1つが別の問題が終了するまで待機する必要がある場合は、マルチスレッド化が適切です(たとえば、I / O負荷の高い操作の場合)。対照的に、サブ問題が同時に発生する可能性がある場合は、マルチプロセッシングをお勧めします。ただし、コアの数を超えるプロセスを作成することはありません。


3

Pythonドキュメントの引用

:私はでスレッドとGIL対プロセスに関する重要なPythonドキュメントの引用を強調してきたCPythonの中でグローバルインタプリタロック(GIL)とは何ですか?

プロセスとスレッドの実験

違いをより具体的に示すために、少しベンチマークを行いました。

ベンチマークでは、8つのハイパースレッド CPU 上のさまざまな数のスレッドについて、CPUとIOの制限付き作業を測定しました。スレッドごとに提供される作業は常に同じであるため、スレッドが多いほど、提供される作業の合計が多くなります。

結果は次のとおりです。

ここに画像の説明を入力してください

データをプロットします

結論:

  • CPUバウンド作業の場合、おそらくGILが原因で、マルチプロセッシングは常に高速です

  • IOバウンド作業用。どちらもまったく同じ速度です

  • 私は8ハイパースレッドマシンを使用しているため、スレッドは期待される8倍ではなく、約4倍までしかスケールアップしません。

    予想される8倍のスピードアップに達するC POSIX CPUバインドの作業とは対照的です:time(1)の出力での「real」、「user」、および「sys」の意味は何ですか?

    TODO:理由はわかりません。他にもPythonの非効率性が関係しているはずです。

テストコード:

#!/usr/bin/env python3

import multiprocessing
import threading
import time
import sys

def cpu_func(result, niters):
    '''
    A useless CPU bound function.
    '''
    for i in range(niters):
        result = (result * result * i + 2 * result * i * i + 3) % 10000000
    return result

class CpuThread(threading.Thread):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class CpuProcess(multiprocessing.Process):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class IoThread(threading.Thread):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

class IoProcess(multiprocessing.Process):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

if __name__ == '__main__':
    cpu_n_iters = int(sys.argv[1])
    sleep = 1
    cpu_count = multiprocessing.cpu_count()
    input_params = [
        (CpuThread, cpu_n_iters),
        (CpuProcess, cpu_n_iters),
        (IoThread, sleep),
        (IoProcess, sleep),
    ]
    header = ['nthreads']
    for thread_class, _ in input_params:
        header.append(thread_class.__name__)
    print(' '.join(header))
    for nthreads in range(1, 2 * cpu_count):
        results = [nthreads]
        for thread_class, work_size in input_params:
            start_time = time.time()
            threads = []
            for i in range(nthreads):
                thread = thread_class(work_size)
                threads.append(thread)
                thread.start()
            for i, thread in enumerate(threads):
                thread.join()
            results.append(time.time() - start_time)
        print(' '.join('{:.6e}'.format(result) for result in results))

GitHubアップストリーム+同じディレクトリ上のプロットコード

Ubuntu 18.10、Python 3.6.7、CPU搭載Lenovo ThinkPad P51ラップトップでテスト済み:Intel Core i7-7820HQ CPU(4コア/ 8スレッド)、RAM:2x Samsung M471A2K43BB1-CRC(2x 16GiB)、SSD:Samsung MZVLB512HAJQ- 000L7(3,000 MB /秒)。

特定の時間に実行されているスレッドを視覚化する

この投稿https://rohanvarma.me/GIL/は、スレッドのがと同じのtarget=引数でthreading.Threadスケジュールされている場合はいつでもコールバックを実行できることを教えてくれましたmultiprocessing.Process

これにより、毎回実行されるスレッドを正確に表示できます。これが完了すると、次のようなものが表示されます(この特定のグラフを作成しました)。

            +--------------------------------------+
            + Active threads / processes           +
+-----------+--------------------------------------+
|Thread   1 |********     ************             |
|         2 |        *****            *************|
+-----------+--------------------------------------+
|Process  1 |***  ************** ******  ****      |
|         2 |** **** ****** ** ********* **********|
+-----------+--------------------------------------+
            + Time -->                             +
            +--------------------------------------+

それはそれを示すでしょう:

  • スレッドはGILによって完全にシリアル化されます
  • プロセスは並行して実行できます

1

これは、スレッド化が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')

私はあなたのコードを使用し(glob部分を削除)、Python 2.6.6でこの興味深い結果を見つけました:>>> do_work(50, 300, 'thread', 'fileio') --> 237.557 ms >>> do_work(50, 300, 'process', 'fileio') --> 323.963 ms >>> do_work(50, 2000, 'thread', 'square') --> 232.082 ms >>> do_work(50, 2000, 'process', 'square') --> 282.785 ms
Alan Garrido

-5

さて、質問のほとんどはジュリオフランコによって答えられます。コンシューマープロデューサーの問題についてさらに詳しく説明します。マルチスレッドアプリを使用するためのソリューションの正しい方向に進むと思います。

fill_count = Semaphore(0) # items produced
empty_count = Semaphore(BUFFER_SIZE) # remaining space
buffer = Buffer()

def producer(fill_count, empty_count, buffer):
    while True:
        item = produceItem()
        empty_count.down();
        buffer.push(item)
        fill_count.up()

def consumer(fill_count, empty_count, buffer):
    while True:
        fill_count.down()
        item = buffer.pop()
        empty_count.up()
        consume_item(item)

同期プリミティブの詳細については、以下を参照してください。

 http://linux.die.net/man/7/sem_overview
 http://docs.python.org/2/library/threading.html

擬似コードは上にあります。より多くの参照を取得するには、producer-consumer-problemを検索する必要があると思います。


申し訳ありませんが、これは私にはC ++のようですか?リンクをありがとう:)
lucacerone 2013

実際、マルチプロセッシングとマルチスレッドの背後にあるアイデアは、言語に依存しません。解決策は上記のコードに似ています。
innosam 2013

2
これはC ++ではありません。それは疑似コード(またはCのような構文を持つほとんど動的に型付けされた言語のコードです。とはいえ、Pythonユーザーに教えるためにPythonのような疑似コードを書く方がより便利だと思います。(特に、Pythonのような疑似コードはしばしば実行可能コード、または少なくともそれに近いコードであることが判明しました。これはCのような疑似コードではめったに当てはまりません…)
abarnert 2013

私はそれをPythonのような擬似コードとして書き直しました(グローバルオブジェクトを使用する代わりにOOを使用してパラメーターを渡すことも)。はっきりしないと思われる場合は、遠慮なく元に戻してください。
abarnert 2013

また、Python stdlibには、これらすべての詳細をまとめた同期キューが組み込まれており、そのスレッドおよびプロセスプールAPIによってさらに抽象化されていることにも注意してください。同期キューが内部でどのように機能するかを理解することは間違いなく価値がありますが、自分で作成する必要はほとんどありません。
abarnert 2013
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.