見出し画像

並列処理、並行処理、非同期処理をPythonで学ぶ

はじめに

申し訳ないのですが、まだ完全に理解しているとは言えない状態なので、間違いがある可能性があるということを承知の上でお読みください。(間違いをコメントなどで指摘していただけるとありがたいです)
また、今回はそれぞれの特徴などを理解するのが目的なのでコードの詳しい解説は行っておりません。できる限り特徴が分かるようなコードを書いたつもりなのですがもし分からない部分がありましたらコメントでご指摘をお願いします。


ざっくりとそれぞれの特徴を確認

並列処理

同時に複数のタスクを実行。
例えば、複数のスレッドやプロセスを使用して、同時に異なる計算を実行することができる。典型的な利用例は、大規模なデータセットを並列に処理して、処理時間を短縮すること。

並行処理

見かけ上同時に複数のタスクを実行。
実際には交互に実行されているシステムのリソースを最適化するために使用される。たとえば、1つのCPUで複数のタスクを交互に実行することで、CPUの使用率を最大限に活用できる。典型的な利用例は、ウェブサーバーが同時に複数のリクエストを処理する場合。同時に多くのユーザーがウェブページにアクセスしても、システムが応答し続けることができる。

非同期処理

待機中のタスクが完了するのを待たずに他のタスクを実行。
非同期処理は、I/Oバウンドな操作やイベント駆動型のアプリケーションで特に有効。たとえば、ファイルの読み書き、ネットワークリクエスト、GUIイベントなどが該当します。非同期処理を使用することで、待機中の処理が完了するのを待つ間に、他の処理を実行できるため、システムの効率が向上する。並行処理の一形態。

詳しくはわかりやすいまとめられている記事がすでにあるのでこちらを参考にしてみてください。

Pythonにおける並列処理、並行処理について

pythonの場合、マルチスレッドは並行処理、マルチプロセスは並列処理になるというのが一般的。どうやらこの辺りは言語によって変わるみたいです。
以下ChatGPTの返答をそのまま記載

マルチスレッド
Pythonのスレッドは、GIL(Global Interpreter Lock)と呼ばれる制約の下で動作します。このため、Pythonのマルチスレッドは、同時に1つのスレッドしか実行できません。したがって、マルチスレッドは厳密には並列処理ではありません。しかし、マルチスレッドは複数のタスクを同時に実行しているように見えるため、一般的には「並行処理」と呼ばれます。
マルチスレッドは、主にI/Oバウンドな操作や同時性が低い問題を解決するために使用されます。I/O操作や待機時間の多い処理は、スレッドがブロックされるため、他のスレッドに制御を移行し、効率的に処理を進行させることができます。
マルチプロセス
マルチプロセスは、複数のプロセスを利用して並行処理を実現します。各プロセスは独立しており、自己完結型であるため、PythonのGILの制約を回避できます。したがって、マルチプロセスは複数のCPUコアを利用して、同時に複数のタスクを実行することができます。このため、マルチプロセスは通常、厳密に言えば並列処理です。
マルチプロセスは、CPUバウンドな操作や並列化可能な問題を解決するために使用されます。

言語によって並行処理と並列処理の定義や解釈は異なる場合があります。これは言語の設計や実装によって異なる特性があるためです。

例えば、PythonではGIL(Global Interpreter Lock)があり、これによってPythonのスレッドが同時に1つのスレッドしか実行できない制約があります。そのため、Pythonにおけるマルチスレッドは一般的には並行処理として捉えられます。しかし、一方でC言語などの多くの言語では、スレッドがマルチコアCPU上で並行に実行されるため、マルチスレッドは並列処理として考えられます。
また、他の言語においても、その言語のランタイムや並行処理のモデルに応じて異なる解釈がされることがあります。例えば、JavaではスレッドがマルチコアCPU上で並行に実行されるため、マルチスレッドは並列処理として扱われることがあります。
そのため、言語によっては「並行処理」と「並列処理」の区別が曖昧になる場合があります。一般的に、プログラミング言語のコミュニティやドキュメントに従って、その言語における並行性や並列性の解釈を理解することが重要です。

ChatGPT

スレッドやプロセスについてはこちらのサイトが分かりやすいで

また、非同期処理はシングルスレッドで動きます。

並列処理

Pythonで並列処理を実行する方法(例)

  • multiprocessingモジュール

  • concurrent.futuresモジュール

  • joblib

concurrent.futures(ProcessPoolExecutor)

コード

from concurrent.futures import ProcessPoolExecutor
import time

def some_task1(task_name, start):
    print(f'{task_name} 開始')
    time.sleep(10)  # 何らかの処理をシミュレートするために10秒間スリープ
    end = time.time()
    print(f'{task_name} 完了{end - start}秒')
    return 'タスク1'

def some_task2(task_name, start):
    print(f'{task_name} 開始')
    time.sleep(15)  # 何らかの処理をシミュレートするために15秒間スリープ
    end = time.time()
    print(f'{task_name} 完了{end - start}秒')
    return 'タスク2'

def some_task3(task_name, start):
    print(f'{task_name} 開始')
    time.sleep(20)  # 何らかの処理をシミュレートするために20秒間スリープ
    end = time.time()
    print(f'{task_name} 完了{end - start}秒')
    return 'タスク3'

def main():
    print("並行処理開始")
    start = time.time()
    # ThreadPoolExecutor を作成
    with ProcessPoolExecutor(max_workers=3) as executor:
        # ここに並列処理したい関数を追加していく
        task_1 = executor.submit(some_task1, 'タスク1', start)   # 引数は第二引数以降に渡す
        task_2 = executor.submit(some_task2, 'タスク2', start)
        task_3 = executor.submit(some_task3, 'タスク3', start)
    end = time.time()
    print(end - start)
    print("並行処理完了")
    print(task_1.result())   # 戻り値を取得
    print(task_2.result())
    print(task_3.result())

if __name__ == '__main__':
    main()

ProcessPoolExecutorを使用する場合、submit()メソッドによって実行される関数は、グローバルスコープ内にある必要がある。関数をグローバルスコープに移動させてから実行することができる。(if name == "main":ブロックでメインの実行部分を保護し、関数の定義部分をグローバルスコープに置く)

実行結果

並行処理開始
タスク1 開始
タスク2 開始
タスク3 開始
タスク1 完了10.119470357894897秒
タスク2 完了15.128458023071289秒
タスク3 完了20.135178327560425秒
20.1542866230011
並行処理完了
タスク1
タスク2
タスク3

タスクマネージャー

ProcessPoolExecutor(1) + max_workers(3)

並行処理(マルチスレッド)

Pythonで並行処理を実行する方法(例)

  • Threading

  • Multiprocessing

  • concurrent.futures

concurrent.futures(ThreadPoolExecutor)

コード


from concurrent.futures import ThreadPoolExecutor
import time

def some_task1(task_name, start):
    print(f'{task_name} 開始')
    time.sleep(10)  # 何らかの処理をシミュレートするために10秒間スリープ
    end = time.time()
    print(f'{task_name} 完了{end - start}秒')
    return 'タスク1'

def some_task2(task_name, start):
    print(f'{task_name} 開始')
    time.sleep(15)  # 何らかの処理をシミュレートするために15秒間スリープ
    end = time.time()
    print(f'{task_name} 完了{end - start}秒')
    return 'タスク2'

def some_task3(task_name, start):
    print(f'{task_name} 開始')
    time.sleep(20)  # 何らかの処理をシミュレートするために20秒間スリープ
    end = time.time()
    print(f'{task_name} 完了{end - start}秒')
    return 'タスク3'

def main():
    print("並列処理開始")
    start = time.time()
    # ThreadPoolExecutor を作成
    with ThreadPoolExecutor(max_workers=3) as executor:
        # ここに並列処理したい関数を追加していく
        task_1 = executor.submit(some_task1, 'タスク1', start)   # 引数は第二引数以降に渡す
        task_2 = executor.submit(some_task2, 'タスク2', start)
        task_3 = executor.submit(some_task3, 'タスク3', start)
    end = time.time()
    print(end - start)
    print("並列処理完了")
    print(task_1.result())   # 戻り値を取得
    print(task_2.result())
    print(task_3.result())

if __name__ == '__main__':
    main()

実行結果

並列処理開始
タスク1 開始
タスク2 開始
タスク3 開始
タスク1 完了10.002028226852417秒
タスク2 完了15.002285957336426秒
タスク3 完了20.0026695728302秒
20.0026695728302
並列処理完了
タスク1
タスク2
タスク3

タスクマネージャー

Pythonは1プロセスのみ

非同期処理

Pythonで非同期処理を実行する方法(例)

  • asyncio

asyncio

asyncio は非同期処理を実現するためのライブラリであり、イベントループを介してシングルスレッドで非同期タスクの実行を制御する。これにより、非同期処理を効率的かつシンプルに実装することができる。

理解が必要なキーワードの説明

  • async
    非同期処理を行う関数(コルーチン)の定義をする

  • await
    非同期関数内でのみ使用され、非同期関数やコルーチンを呼び出す際に使用される。awaitを使うことで、呼び出した非同期関数の処理が完了するまで、その場所で非同期関数の実行を一時停止させることができる。(つまり他の処理を実行することができる)

コード

import asyncio
import time

async def asyncio_sleep(task, num):
    print(f"{task} 待機開始")
    await asyncio.sleep(num)

async def task1():
    print("Task 1: Starting")
    time.sleep(5)
    await asyncio_sleep("task1", 5)  # 5秒間遅延
    print("Task 1: Finished")

async def task2():
    print("Task 2: Starting")
    await asyncio_sleep("task2", 10)  # 10秒間遅延
    print("Task 2: Finished")


async def main():
    await asyncio.gather(task1(), task2())

if __name__ == '__main__':
    start = time.time()
    asyncio.run(main())
    print(time.time() - start)

実行結果

Task 1: 開始
task 1-1開始
task 1-1終了
task 1-2 開始
Task 2: 開始
task2-1 開始
Task 1-2: 終了
task 1 終了
task 2-1 終了
Task 2: 終了
15.018834829330444

処理の流れ

  1. task1開始

  2. task1-1開始:time.sleepは非同期関数ではないためtask2は実行できない

  3. task1-1終了

  4. task1-2開始:asyncio_sleepは非同期関数なのでtask2も実行開始

  5. task2開始

  6. task2-1開始(task1-2とtask2-1は並行的に処理される)

  7. task1-2終了

  8. task1終了

  9. task2-1終了

  10. task2終了

asyncioを使った非同期処理はシングルスレッドで実行される。そのためI/O操作や、ネットワーク通信、ファイル操作、データベース操作など処理の途中で時間のかかる待機が発生する処理にawaitをつけて待機中に他の処理を実行する。計算処理などのCPUバウンドの処理については効果的ではない。

学習の際に視聴した動画


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