web-dev-qa-db-ja.com

マルチプロセッシングとスレッドPython

マルチプロセッシング over threading の利点を理解しようとしています。 multiprocessing がGlobal Interpreter Lockを回避することはわかっていますが、他にどのような利点がありますか。 threading で同じことができないのですか。

664
John

threadingモジュールはスレッドを使用し、multiprocessingモジュールはプロセスを使用します。違いは、スレッドは同じメモリ空間で実行されるのに対し、プロセスは別々のメモリを持つということです。これにより、マルチプロセッシングを使用してプロセス間でオブジェクトを共有することが少し難しくなります。スレッドは同じメモリを使用するため、予防措置を講じる必要があります。そうしないと、2つのスレッドが同時に同じメモリに書き込みます。これがグローバルインタプリタロックの目的です。

プロセスの生成は、スレッドの生成よりも少し遅くなります。それらが実行されると、それほど大きな違いはありません。

595
Sjoerd

これが私が思いついたいくつかの賛否両論です。

マルチプロセッシング

長所

  • 独立したメモリ空間
  • コードは通常簡単です
  • 複数のCPUとコアを利用する
  • CPythonのGIL制限を回避
  • 共有メモリを使用しているのでない限り、同期プリミティブの必要性をほとんど排除します(代わりに、IPCの通信モデルになります)。
  • 子プロセスは割り込み可能/強制終了可能
  • Pythonのmultiprocessingモジュールには、threading.Threadのようなインターフェースを持つ便利な抽象化が含まれています。
  • CPUバウンド処理のためのcPythonの必要条件

短所

  • IPCは、オーバーヘッドが増えるともう少し複雑になります(通信モデルと共有メモリ/オブジェクト)。
  • より大きなメモリフットプリント

スレッディング

長所

  • 軽量 - メモリ使用量が少ない
  • 共有メモリ - 他のコンテキストから状態へのアクセスを容易にします
  • レスポンシブUIを簡単に作成できる
  • gILを正しく解放するcPython C拡張モジュールは並行して実行されます
  • I/Oバウンドアプリケーションに最適なオプション

短所

  • cPython - GILの対象
  • 割り込まない/殺す
  • Queueモジュールを使用して)コマンドキュー/メッセージポンプモデルに従わない場合は、同期プリミティブを手動で使用する必要があります(ロックの細分性には決定が必要です)。
  • コードは通常、理解して正しく理解するのが困難です - 競合状態の可能性は劇的に高まります
734
Jeremy Brown

スレッディングの仕事はアプリケーションが応答することを可能にすることです。データベースに接続していて、ユーザー入力に応答する必要があるとします。スレッドがないと、データベース接続がビジーの場合、アプリケーションはユーザーに応答できません。データベース接続を別のスレッドに分割することで、アプリケーションの応答性を向上させることができます。また、両方のスレッドが同じプロセス内にあるため、同じデータ構造にアクセスできます - 優れたパフォーマンスと柔軟なソフトウェア設計。

GILのため、アプリケーションは実際には一度に2つのことをしているわけではありませんが、データベースのリソースロックを別のスレッドに配置して、CPU時間をユーザー操作と切り替えることができます。 CPU時間はスレッド間で配分されます。

マルチプロセッシングは、いつでも複数のことを実行したい場合があります。アプリケーションが6つのデータベースに接続し、各データセットに対して複雑な行列変換を実行する必要があるとします。 1つの接続がアイドル状態のときに別のスレッドにCPU時間がかかる可能性があるため、各ジョブを別々のスレッドに配置すると少し役に立ちますが、GILは1つのCPUのリソースしか使用しないため。各ジョブをマルチプロセッシングプロセスに入れることで、それぞれを独自のCPUで実行し、最大限の効率で実行できます。

189
Simon Hibbs

主な利点は分離です。クラッシュしたプロセスは他のプロセスをダウンさせることはありませんが、クラッシュしたスレッドはおそらく他のスレッドを破壊するでしょう。

39
Marcelo Cantos

言及されていないもう一つのことは、速度が関係しているところであなたがどのOSを使っているかに依存するということです。 Windowsではプロセスはコストがかかるのでウィンドウではスレッドのほうが優れていますが、unixプロセスではウィンドウズよりも速いので、unixでプロセスを使用する方がはるかに安全で素早く生成できます。

26
chrissygormley

他の答えは、マルチスレッドとマルチプロセッシングの側面に重点を置いていますが、PythonのGlobal Interpreter Lock(GIL)を考慮する必要があります。もっと数が多いと k)のスレッドが作成されます、一般的に彼らはパフォーマンスを向上させることはありません k まだシングルスレッドのアプリケーションとして実行されているためです。 GILは、すべてをロックアウトし、単一コアのみを利用して単一スレッドの実行のみを許可するグローバルロックです。 Numpy、Network、I/OなどのC拡張が使用されている場所、多くのバックグラウンド作業が行われてGILがリリースされている場所ではパフォーマンスが向上します。
そうするとき スレッディング pythonを使用すると、オペレーティングシステムレベルのスレッドは1つしかありませんが、pythonは、スレッド自体によって完全に管理され、本質的に単一のプロセスとして実行されている擬似スレッドを作成します。プリエンプションはこれらの疑似スレッド間で行われます。 CPUが最大容量で稼働している場合は、マルチプロセッシングに切り替えることをお勧めします。
自己完結型の実行インスタンスの場合は、代わりにプールを選択できます。しかし、データが重複している場合、プロセス間の通信が必要な場合はmultiprocessing.Processを使用する必要があります。

17

質問で述べたように、PythonのMultiprocessingは、真の並列処理を実現するための唯一の現実的な方法です。 _ gil _ はスレッドの並列実行を妨げるため、マルチスレッドはこれを達成できません。

結果として、スレッド化はPythonでは必ずしも有用ではないかもしれません、そして実際に、あなたが達成しようとしているものによってはさらに悪いパフォーマンスをもたらすかもしれません。たとえば、gzipファイルの解凍や3Dレンダリング(CPUを多用するものなど)などの CPU-bound タスクを実行している場合、スレッド処理は実際にはヘルプよりもパフォーマンスを妨げる可能性があります。このような場合は、実際にはこのメソッドだけが並行して実行され、当面のタスクの重みを分散させるのに役立つので、Multiprocessingを使用することをお勧めします。 Multiprocessingはスクリプトのメモリを各サブプロセスにコピーすることを伴うので、これにはいくらかのオーバーヘッドがあるかもしれません。そして、それはより大きなサイズのアプリケーションにとって問題を引き起こすかもしれません。

ただし、マルチスレッドは、タスクが IO-bound の場合に役立ちます。たとえば、タスクの大部分が API-call の待機を伴う場合は、Multithreadingを使用することになります。のどかに座る。

TL; DR

  • マルチスレッドは並行しており、 IO限界 タスクに使用されます
  • Multiprocessingは真の並列処理を実現し、 CPUバウンド タスクに使用されます。
10
Bolboa

Pythonドキュメントの引用符

Process vs ThreadsとGILに関するPythonドキュメントの重要な引用箇所を以下で強調しました。 CPythonのグローバルインタープリタロック(GIL)とは何ですか?

プロセスvsスレッド実験

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

ベンチマークでは、 8ハイパースレッド CPU上でCPUとIOがさまざまな数のスレッドに対して動作するようにタイミングを合わせました。スレッドごとに提供される作業は常に同じです。スレッドが多いほど、提供される作業の合計が多くなります。

結果は次のとおりです。

enter image description here

結論:

  • cPUに制約のある作業では、マルチプロセッシングは常に高速です。おそらくGILによるものです。

  • IOバインド作業用。両方ともまったく同じ速度

  • 私が8ハイパースレッドマシンを使っているので、スレッドは予想される8倍ではなく約4倍にしかスケールアップしません。

    それとは対照的に、期待される8倍のスピードアップを達成するC POSIX CPUバウンドの仕事とは対照的です。 'real'、 'user'、および 'sys'はtime(1)の出力に何を意味しますか?

    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 __== '__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サムスンM471A2K43BB1-CRC(2x 16GiB)、SSD:サムスンMZVLB512HAJQ- 000L7(3,000 MB /秒).

プロセスは複数のスレッドを持つことができます。これらのスレッドはメモリを共有し、プロセス内の実行単位です。

プロセスはCPU上で実行されるため、スレッドは各プロセスの下に常駐しています。プロセスは独立して実行される個々のエンティティです。各プロセス間でデータや状態を共有したい場合は、Cache(redis, memcache)FilesDatabaseなどのメモリ保存ツールを使用できます。

_マルチプロセッシング_

  • マルチプロセッシングは、計算能力を高めるためにCPUを追加します。
  • 複数のプロセスが同時に実行されます。
  • プロセスの作成は時間がかかり、リソースを消費します。
  • 多重処理は対称的でも非対称的でもよい。
  • Pythonのマルチプロセッシングライブラリは、独立したメモリ空間、複数のCPUコアを使用し、CPythonのGIL制限を回避し、子プロセスは強制終了でき(プログラム内の関数呼び出しなど)、はるかに使いやすくなります。
  • このモジュールに関する注意点の中には、メモリ使用量が大きくなること、およびIPCのオーバーヘッドが増えて少し複雑になることがあります。

_マルチスレッド_

  • マルチスレッドは、計算能力を高めるために単一プロセスの複数のスレッドを作成します。
  • 単一プロセスの複数のスレッドが同時に実行されます。
  • スレッドの作成は、意味のある時間とリソースの両方で経済的です。
  • マルチスレッドライブラリは軽量で、メモリを共有し、レスポンシブUIを担当し、I/Oバウンドアプリケーションによく使用されます。
  • このモジュールは殺害されず、GILの影響を受けます。
  • 複数のスレッドが同じスペースの同じプロセスに存在し、各スレッドは特定のタスクを実行し、独自のコード、独自のスタックメモリ、命令ポインタ、および共有ヒープメモリを持ちます。
  • スレッドにメモリリークがあると、他のスレッドや親プロセスに損傷を与える可能性があります。

Pythonを使ったマルチスレッドとマルチプロセッシングの例

Python 3は 並列タスクの起動 の機能を持っています。これにより作業が容易になります。

スレッドプーリング および プロセスプーリング があります。

以下は洞察を与えます:

ThreadPoolExecutorの例

import concurrent.futures
import urllib.request

URLS = ['http://www.foxnews.com/',
        'http://www.cnn.com/',
        'http://europe.wsj.com/',
        'http://www.bbc.co.uk/',
        'http://some-made-up-domain.com/']

# Retrieve a single page and report the URL and contents
def load_url(url, timeout):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()

# We can use a with statement to ensure threads are cleaned up promptly
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    # Start the load operations and mark each future with its URL
    future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
    for future in concurrent.futures.as_completed(future_to_url):
        url = future_to_url[future]
        try:
            data = future.result()
        except Exception as exc:
            print('%r generated an exception: %s' % (url, exc))
        else:
            print('%r page is %d bytes' % (url, len(data)))

ProcessPoolExecutor

import concurrent.futures
import math

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for number, prime in Zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))

if __== '__main__':
    main()
1
Jeril

私が大学で学ぶとき、上の答えのほとんどは正しいです。異なるプラットフォームでのプラクティスでは(常にpythonを使用します)、複数のスレッドを生成することは、1つのプロセスを生成することのようになります。違いは、1つのコアですべてを100%で処理するのではなく、複数のコアが負荷を共有することです。あなたが4コアのPC上で例えば10のスレッドを生成したのであれば、あなたはCPUパワーの25%しか得られなくなるでしょう!そして、もしあなたが10個のプロセスを生成するならば、あなたは100%のcpu処理で終わるでしょう(あなたが他の制限を持たないなら)。私はすべての新技術の専門家ではありません。自分自身の実際の経験の背景を持つIM

0
Alex