Python の同時実行性の違い

Marion Paul Kenneth Mendoza 2023年6月21日
Python の同時実行性の違い

Python 3 のリリースに伴い、非同期操作と同時実行性に関する多くの新しいトレンドを聞いていることを考えると、Python はこれらの概念や機能を導入したばかりだと想像するかもしれません。

多くの新参者は、asyncio を使用することが同時実行および非同期アクティビティを実行するための唯一の実用的なアプローチであると考えるかもしれません。 この記事では、Python で並行性を実現する方法と、その利点または欠点について説明します。

スレッドとマルチスレッド

スレッドは非常に長い間 Python にありました。 その結果、スレッドのおかげで一度に複数の操作を実行できます。

残念ながら、典型的なメインラインの Python バージョンである CPython は、グローバル インタープリター ロック (GIL) をまだ使用しているため、マルチスレッド アプリケーション (並列処理を実装する現在の一般的な方法) は理想的とは言えません。

Python は GIL を導入して、CPython のメモリ処理を C との統合 (拡張機能など) により管理しやすくしました。

GIL は、Python インタープリターが同時に 1つのスレッドのみを実行するロック メカニズムです。 Python の byte コードは、同時に 1つのスレッドでしか実行できません。

コード例:

import threading
import time
import random


def worker(num):
    sec = random.randrange(1, 5)
    time.sleep(sec)
    print("I am thread {}, who slept for {} seconds.".format(num, sec))


for i in range(3):
    t = threading.Thread(target=worker, args=(i,))
    t.start()

print("Completed!")

出力:

Completed!
I am thread 1, who slept for 3 seconds.
I am thread 3, who slept for 2 seconds.
I am thread 4, who slept for 4 seconds.

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

マルチプロセッシングは多くの CPU を利用します。 各CPUが並行して動作するため、複数のタスクを同時に効率的に実行できます。 CPU バウンドのジョブの場合、マルチプロセッシングを使用する必要があります。

Python では、並列処理を実現するために multiprocessing モジュールが導入されています。

コード例:

import multiprocessing
import time
import random


def worker(num):
    sec = random.randrange(1, 5)
    time.sleep(sec)
    print("I am process {}, who slept for {} seconds.".format(num, sec))


for i in range(3):
    t = multiprocessing.Process(target=worker, args=(i,))
    t.start()

print("Completed")

出力:

Completed
I am process 1, who slept for 1 seconds.
I am process 2, who slept for 2 seconds.
I am process 0, who slept for 3 seconds.

マルチスレッドの代わりに、CPU の異なるコアで実行される複数のプロセスを使用しているため、Python スクリプトが高速になります。

非同期と asyncio

同期操作では、タスクは次々と同期して実行されます。 ただし、ジョブは互いに完全に独立して非同期操作で開始される場合があります。

実行が別のアクティビティに切り替わっている間、1つの async タスクが開始され、実行が継続される場合があります。 一方、非同期タスクは多くの場合、バックグラウンドで実行され、ブロックされません (実装を完了まで待機させます)。

他の価値ある機能とともに、asyncio はイベント ループを提供します。 イベント ループは、さまざまな I/O イベントを監視し、準備完了のタスクに切り替え、I/O を待っているタスクを一時停止します。

その結果、未完成のプロジェクトに時間を費やすことはありません。

コード例:

import asyncio
import datetime
import random


async def my_sleep_func():
    await asyncio.sleep(random.randint(0, 5))


async def displayDate(num, loop):
    endTime = loop.time() + 60.0
    while True:
        print("Loop: {} Time: {}".format(num, datetime.datetime.now()))
        if (loop.time() + 1.0) >= endTime:
            break
        await my_sleep_func()


loop = asyncio.get_event_loop()

asyncio.ensure_future(displayDate(1, loop))
asyncio.ensure_future(displayDate(2, loop))

loop.run_forever()

上記のコード スニペットを見ていくと、次のようになります。

  • async 関数 displayDate があります。これは、数値とイベント ループをパラメーターとして受け取ります。
  • 上記の関数には、60 秒後に停止する無限ループがあります。 しかし、この 60 秒間、繰り返し時間を出力し、昼寝をします。
  • await 関数は、他の async 関数が完了するのを待つことができます。
  • 関数をイベント ループに渡します (ensure_future 関数を使用)。
  • イベント ループの実行を開始します。

await 呼び出しが行われるたびに、asyncio は関数がおそらくある程度の時間を必要とすることを理解します。 asyncio は、停止していた関数の I/O の準備が整ったことに気付くと、プロセスを再開します。

ここで重要なのは、3つの形式の同時実行のうち、何を使用する必要があるかということです。 意思決定に役立てるために、次の点に注意してください。

  • CPU バウンド操作にはマルチプロセッシングを使用します。
  • I/O バウンド、高速 I/O、および限定数の接続にはマルチスレッドを使用します。
  • I/O バウンド、低速 I/O、および多数の接続には非同期 IO を使用します。
  • asyncio/await は Python 3.5 以降で動作します。

以下の疑似コードも参照できます。

if io_bound:
    if io_very_slow:
        print("Use asyncio")
    else:
        print("Use multithreading")
else:
    print("multiprocessing")
Marion Paul Kenneth Mendoza avatar Marion Paul Kenneth Mendoza avatar

Marion specializes in anything Microsoft-related and always tries to work and apply code in an IT infrastructure.

LinkedIn

関連記事 - Python Threading

関連記事 - Python Async