見出し画像

python マルチプロセスとマルチスレッドについて

マルチプロセスとマルチスレッドは、コンピュータの複数のタスクを並行して実行するための二つの異なる方法です。これらの概念を理解するためには、まずプロセスとスレッドの基本的な違いから始めましょう。

プロセス

プロセスは、実行中のプログラムのインスタンスです。プロセスは自分自身のメモリ空間を持ち、オペレーティングシステムからシステムリソース(CPU時間、メモリなど)を割り当てられます。異なるプロセス間ではメモリが共有されないため、プロセス間の通信には特別な方法(例えば、IPC(Inter-Process Communication)メカニズム)が必要です。

スレッド

スレッドは、プロセス内で実行される実行の流れの単位です。すべてのプロセスは少なくとも一つのスレッド(メインスレッド)を持ちますが、複数のスレッドを作成して同時に複数のタスクを実行することができます。スレッドはプロセスのリソース(メモリなど)を共有するため、スレッド間の通信やデータの共有が容易です。

マルチプロセス

マルチプロセスは、複数のプロセスを同時に実行することによって、タスクを並行して処理します。各プロセスは独立しているため、一つのプロセスで発生した問題が他のプロセスに影響を与えることはありません。しかし、プロセス間の通信は比較的コストが高く、リソースの使用量も多くなりがちです。

マルチスレッド

マルチスレッドは、単一のプロセス内で複数のスレッドを使用してタスクを並行して処理します。スレッド間でメモリなどのリソースを共有するため、通信やデータの共有が効率的に行えます。しかし、スレッド間のリソースの共有により、データの不整合が発生する可能性があり、それを管理するための同期が必要になります。

比較と使用シナリオ

  • リソースの使用量:マルチプロセスは各プロセスが独立したメモリ空間を持つため、マルチスレッドに比べてリソースの使用量が多くなります。

  • 安定性と障害隔離:マルチプロセスはプロセスが独立しているため、一つのプロセスのクラッシュが他のプロセスに影響を与えにくいです。一方、マルチスレッドでは一つのスレッドの問題がプロセス全体に影響を与える可能性があります。

  • 通信のコストと複雑さ:マルチスレッドはスレッド間でリソースを共有するため、プロセス間の通信よりも効率的ですが、データの不整合を避けるための同期の必要性が増します。

選択は、アプリケーションの要件、安定性、パフォーマンス、開発の複雑さなど、さまざまな要因に基づいて行うべきです。具体的には:

  • 高い障害隔離が必要な場合:システムの一部がクラッシュしても他の部分に影響を与えたくない場合、マルチプロセス方式が適しています。例えば、ウェブサーバーでは、個々のリクエストを別々のプロセスで処理することで、一つのリクエストがサーバー全体をダウンさせるリスクを減らすことができます。

  • リソースの利用効率とスピードが重視される場合:共有メモリへのアクセスが頻繁に必要なアプリケーションや、リアルタイム処理が求められる場合などは、マルチスレッド方式が有利です。例えば、グラフィックス処理やゲームエンジンでは、複数のスレッドが共通のデータセットを操作しながら、画面の描画や物理計算を並行して行います。

  • 開発の複雑さとデバッグ:マルチスレッドプログラムは、スレッド間でのリソース共有によるデータの不整合や、デッドロックのような同期問題を適切に管理する必要があるため、開発とデバッグがより複雑になる可能性があります。これに対して、マルチプロセスプログラムはプロセス間の隔離が保証されているため、これらの問題を避けやすいですが、プロセス間通信のコストがかかります。

実践的な考慮事項

  • プロセス間通信(IPC):マルチプロセス設計では、プロセス間でデータをやり取りするためにIPCメカニズム(例: ソケット通信、共有メモリ、パイプなど)が必要になります。これは、プロセスが独立したメモリ空間を持っているためです。

  • 同期メカニズム:マルチスレッド設計では、スレッド間で共有されるデータへのアクセスを管理するために、ロックやセマフォ、ミューテックスなどの同期メカニズムを使用します。これにより、同時に複数のスレッドが同じデータにアクセスし、予期せぬ問題が発生するのを防ぎます。

最終的に、マルチプロセスとマルチスレッドの選択は、アプリケーションの特定の要件と、開発チームの技術的な適性や好みに依存します。効率、安定性、開発の容易さの間で適切なバランスを見つけることが重要です。

python のソースコード例


マルチプロセスの例

from multiprocessing import Process
import time

# この関数は各プロセスで実行されます
def print_square(numbers):
    for n in numbers:
        time.sleep(0.5) # シミュレーションのために少し待ちます
        print(f'Square: {n * n}')

# この関数も各プロセスで実行されます
def print_cube(numbers):
    for n in numbers:
        time.sleep(0.5) # シミュレーションのために少し待ちます
        print(f'Cube: {n * n * n}')

if __name__ == "__main__":
    numbers = [2, 3, 4, 5]

    # プロセスを作成します
    p1 = Process(target=print_square, args=(numbers,))
    p2 = Process(target=print_cube, args=(numbers,))

    # プロセスを開始します
    p1.start()
    p2.start()

    # 各プロセスの終了を待ちます
    p1.join()
    p2.join()

    print("Done!")

このコードでは、print_square関数とprint_cube関数を並行して実行するために2つのプロセスを作成しています。各プロセスは独立しており、自分のタスクを実行します。

具体的な説明は以下になります

def print_square(numbers):
    for n in numbers:
        time.sleep(0.5)
        print(f'Square: {n * n}')

def print_cube(numbers):
    for n in numbers:
        time.sleep(0.5)
        print(f'Cube: {n * n * n}')
  • print_squareprint_cube 関数は、それぞれ与えられた数値のリストに対して平方と立方を計算し、結果を出力します。

  • time.sleep(0.5) は、各計算の間に0.5秒間の遅延を挿入しています。これにより、プロセスが実際に並行して実行されていることが視覚的に確認しやすくなります。

if __name__ == "__main__":
    numbers = [2, 3, 4, 5]

    p1 = Process(target=print_square, args=(numbers,))
    p2 = Process(target=print_cube, args=(numbers,))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    print("Done!")
  • if __name__ == "__main__": この行は、スクリプトが直接実行された場合のみコードブロックを実行するようにします。

  • numbers は、平方と立方を計算するための数値リストです。

  • Process オブジェクト p1p2 を作成します。それぞれ print_squareprint_cube 関数を実行するためのものです。target 引数には実行する関数を、args 引数にはその関数に渡す引数を指定します。

  • start() メソッドを呼び出してプロセスを開始します。

  • join() メソッドを使って、p1p2 の両プロセスが終了するまでメインプログラムが待機するようにします。これにより、プログラムが print("Done!") を出力する前に、両方のプロセスの実行が完了することが保証されます。

イベントとキューについて

イベント

  • イベントは、特定の条件が満たされたことをプロセスやスレッド間で通知するために使用されます。例えば、あるタスクが完了したことを他のタスクに知らせたい場合に使用します。イベントは内部的にフラグを持ち、このフラグはセット(set()メソッド)されたりクリア(clear()メソッド)されたりします。フラグがセットされている状態を待つプロセスやスレッドは、イベントのwait()メソッドを使って待機します。フラグがセットされると、wait()メソッドは待機を解除し、プログラムの実行を続けます。

イベントは、例えばデータの準備が完了したことや、特定の条件が成立したことを他のプロセスやスレッドに通知する際に非常に有用です。これにより、必要なタイミングでのみ処理を進めることができ、無駄なリソースの消費を避けることができます。

キュー

キューは、プロセスやスレッド間でデータを安全に交換するために使用されるデータ構造です。queue.Queueクラスはスレッドセーフで、multiprocessing.Queueはプロセスセーフです。これらのキューは、プロデューサ(データをキューに追加するプロセスやスレッド)とコンシューマ(キューからデータを取り出して処理するプロセスやスレッド)間でのデータのやり取りをサポートします。

キューを使用することで、データの生成と消費を異なるプロセスやスレッドで平行して行うことができます。例えば、一つのプロセスがファイルからデータを読み込んでキューに追加し、別のプロセスがそのキューからデータを取り出して処理する、というような使い方が考えられます。これにより、効率的なデータ処理のパイプラインを構築することができます。

マルチプロセスとマルチスレッドにおけるイベントとキューの使用

マルチプロセスやマルチスレッド環境では、イベントとキューは並行処理の同期やデータの交換に不可欠なツールです。イベントを使用することで、異なるプロセスやスレッドが特定の条件やタイミングで動作を開始したり、停止したりすることができます。キューを通じてデータをやり取りすることで、データの整合性を保ちながら、複数のプロセスやスレッドが効率的に協力してタスクを実行することが可能になります。

これらの同期メカニズムは、複雑な並行処理アプリケーションの設計において、データの整合性を保ちつつ、リソースの利用を最大化するために重要な役割を果たします。


マルチプロセスにおけるイベントとキューを組み込んだソースコード

from multiprocessing import Process, Event, Queue
import time

# イベントとキューを使用するための関数
def print_square(numbers, start_event, data_queue):
    start_event.wait()  # メインプロセスからの開始信号を待ちます
    for n in numbers:
        time.sleep(0.5)  # シミュレーションのために少し待ちます
        result = n * n
        print(f'Square: {result}')
        data_queue.put(('Square', result))  # 結果をキューに追加します

def print_cube(numbers, start_event, data_queue):
    start_event.wait()  # メインプロセスからの開始信号を待ちます
    for n in numbers:
        time.sleep(0.5)  # シミュレーションのために少し待ちます
        result = n * n * n
        print(f'Cube: {result}')
        data_queue.put(('Cube', result))  # 結果をキューに追加します

if __name__ == "__main__":
    numbers = [2, 3, 4, 5]

    # イベントとキューの作成
    start_event = Event()
    data_queue = Queue()

    # プロセスを作成します
    p1 = Process(target=print_square, args=(numbers, start_event, data_queue))
    p2 = Process(target=print_cube, args=(numbers, start_event, data_queue))

    # プロセスを開始します
    p1.start()
    p2.start()

    # 計算開始前に少し待ちます(オプション)
    time.sleep(1)
    print("Starting tasks...")
    start_event.set()  # タスク開始のイベントをセットします

    # 各プロセスの終了を待ちます
    p1.join()
    p2.join()

    # キューから結果を取り出します
    while not data_queue.empty():
        item = data_queue.get()
        print(f"Processed {item[0]}: {item[1]}")

    print("Done!")

put()関数
put()メソッドは、キューにアイテムを追加するために使用されます。この例では、data_queueはプロセス間で共有されるキューであり、各子プロセスは計算結果をキューに追加する際にput()メソッドを呼び出します。put()メソッドに渡されるデータは、タプル(('Square', result)や('Cube', result))の形式で、計算の種類(平方または立方)とその結果を含みます。

get()関数
get()メソッドは、キューからアイテムを取り出すために使用されます。この例では、メインプロセスがget()メソッドを使用して、data_queueから計算結果を順番に取り出し、それらの結果を表示します。get()メソッドはキューからアイテムを取り出し、そのアイテムを返します。もしキューが空の場合は、新しいアイテムが追加されるまで待機します(ただし、この例ではdata_queue.empty()メソッドを使用してキューが空かどうかを確認し、空でない場合のみget()を呼び出しています)。

処理の流れの要約
イベントで待機: 各子プロセスは、start_event.wait()を使ってメインプロセスからの開始信号を待ちます。
計算とキューへの追加: 開始信号を受け取ると、子プロセスはそれぞれの計算タスクを実行し、結果をdata_queue.put(('Square', result))やdata_queue.put(('Cube', result))を使ってキューに追加します。
キューからの取り出しと表示: メインプロセスは、while not data_queue.empty():ループを使用してキューが空になるまで、data_queue.get()を呼び出し、取り出した結果を表示します。
このようにして、EventとQueueを使用することで、プロセス間での同期とデータの安全な交換が行われ、複数のプロセスを効率的に管理することが可能になります。


マルチスレッドの例

from threading import Thread, Event
from queue import Queue
import time

# この関数は各スレッドで実行されます
def print_square(numbers, start_event, data_queue):
    start_event.wait()  # メインスレッドからの開始信号を待ちます
    for n in numbers:
        time.sleep(0.5)  # シミュレーションのために少し待ちます
        result = n * n
        print(f'Square: {result}')
        data_queue.put(('Square', result))  # 結果をキューに追加します

def print_cube(numbers, start_event, data_queue):
    start_event.wait()  # メインスレッドからの開始信号を待ちます
    for n in numbers:
        time.sleep(0.5)  # シミュレーションのために少し待ちます
        result = n * n * n
        print(f'Cube: {result}')
        data_queue.put(('Cube', result))  # 結果をキューに追加します

if __name__ == "__main__":
    numbers = [2, 3, 4, 5]

    # イベントとキューの作成
    start_event = Event()
    data_queue = Queue()

    # スレッドを作成します
    t1 = Thread(target=print_square, args=(numbers, start_event, data_queue))
    t2 = Thread(target=print_cube, args=(numbers, start_event, data_queue))

    # スレッドを開始します
    t1.start()
    t2.start()

    # タスク開始のイベントをセットします
    time.sleep(1)  # 実際のタスク開始前に少し待ちます(オプション)
    print("Starting tasks...")
    start_event.set()

    # 各スレッドの終了を待ちます
    t1.join()
    t2.join()

    # キューから結果を取り出します
    while not data_queue.empty():
        item = data_queue.get()
        print(f"Processed {item[0]}: {item[1]}")

    print("Done!")

このコードでは、threading.Threadを使用してスレッドを作成し、threading.Eventqueue.Queueを使ってスレッド間で同期とデータの交換を行っています。このような方式は、異なるタスクを複数のスレッドで並行して実行し、その結果を集約する場合に有用です。また、Eventオブジェクトを使用することで、すべてのスレッドが特定のイベント(この場合は計算タスクの開始)に同期して動作を開始することが可能になります。

まとめ


マルチプロセス

  • 定義: マルチプロセスは、複数のプロセスが並列に実行されることです。プロセスは、実行中のプログラムのインスタンスであり、それぞれが独自のアドレス空間(メモリ)を持ちます。

  • 隔離性: 各プロセスは独立しているため、一つのプロセスで発生したエラーやクラッシュが他のプロセスに影響を与えることはありません。

  • 通信: プロセス間でデータを共有するには、プロセス間通信(IPC)メカニズム(例:パイプ、ソケット、共有メモリ)を使用する必要があります。

  • 使用シナリオ: 高い障害隔離が必要な場合や、異なるプログラミング言語で書かれたプログラム間で作業を分割する必要がある場合に適しています。

マルチスレッド

  • 定義: マルチスレッドは、単一のプロセス内で複数のスレッドが同時に実行されることです。スレッドは軽量プロセスとも呼ばれ、プロセス内の実行の流れを表します。

  • 共有メモリ: スレッド間でプロセスのメモリ(データ、ヒープ空間など)が共有されるため、データの共有と通信が容易ですが、同期のための追加的な注意が必要です(例:ロックメカニズムを使用してデータ競合を避ける)。

  • リソース: メモリやリソースのオーバーヘッドがプロセスに比べて少なく、コンテキストスイッチ(タスクの切り替え)のコストも低いです。

  • 使用シナリオ: リアルタイム処理や高速なデータ共有が必要なアプリケーション(例:GUIアプリケーション、サーバー)に適しています。

ソースコードの類似性

マルチプロセスとマルチスレッドのプログラムのソースコードが類似している理由は、Python(および多くのプログラミング言語)が提供する抽象化のおかげです。multiprocessingthreadingモジュールは、非常に似たAPI(例:ProcessThreadクラス、start()join()メソッド)を提供しており、プログラマが似た構造のコードを書くことで、異なる並行実行モデルを採用できるようにしています。

この記事が気に入ったらサポートをしてみませんか?