Skip to content

8. Thread, Multi Process-多重化/並列処理/非同期処理

多重化とか並行処理とか並列処理とか

この手の処理方法、いろいろな言葉が出てきますので整理します。

呼称 使用モジュール GIL 備考
マルチスレッド threading 影響大 1CPUで複数処理が可能
マルチプロセス multiprocessing.Process 影響少 1CPU1処理。並列度はCPU数に依存
非同期処理 asyncio

I/Oバウンドなタスクを並行して複数実行したい場合にはマルチスレッドが、CPUバウンドな処理を複数実行したい場合はマルチプロセスが正しい選択肢となります。

8-1. マルチスレッド

スレッドはプロセス内で共有されるメモリを使用する。
同じメモリ空間内で動作するため、データを共有しやすいが、競合に注意が必要。
グローバル・インタプリタ・ロック(GIL)により、Pythonでは多くの場合、複数のスレッドが同時に実行されない。

import threading
import time

def print_numbers():
    for i in range(5):
        time.sleep(1)
        print(f"Thread 1: {i}")

def print_letters():
    for letter in 'ABCDE':
        time.sleep(1)
        print(f"Thread 2: {letter}")

# マルチスレッドの作成
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)

# スレッドの開始
thread1.start()
thread2.start()

# メインスレッドの終了を待つ
thread1.join()
thread2.join()

print("マルチスレッドの処理が完了しました。")
$ python py8_1.py
Thread 1: 0
Thread 2: A
Thread 2: B
Thread 1: 1
Thread 2: C
Thread 1: 2
Thread 1: 3
Thread 2: D
Thread 1: 4
Thread 2: E
マルチスレッドの処理が完了しました。

下記のように引数を渡す記述をすると、GILの関係でマルチスレッドで動作しなくなる。

import threading
import time

def print_numbers(pid1):
    for i in range(5):
        time.sleep(1)
        print(f"Thread {pid}: {i}")

def print_letters(pid2):
    for letter in 'ABCDE':
        time.sleep(1)
        print(f"Thread {pid}: {letter}")

# マルチスレッドの作成
thread1 = threading.Thread(target=print_numbers(1))
thread2 = threading.Thread(target=print_letters(2))

# スレッドの開始
thread1.start()
thread2.start()

# メインスレッドの終了を待つ
thread1.join()
thread2.join()

print("マルチスレッドの処理が完了しました。")
$ python py8_1a.py
Thread 1: 0
Thread 1: 1
Thread 1: 2
Thread 1: 3
Thread 1: 4
Thread 2: A
Thread 2: B
Thread 2: C
Thread 2: D
Thread 2: E
マルチスレッドの処理が完了しました。

ランダムな秒数の待機を行う処理と並行して1秒ごとに経過時間を出力してみる
"progress:X" の表示は1秒おきに出力。
その間にランダムなタイミングで "StepX" が出力される。

import threading
import time
import random

def print_numbers():
    for i in range(5):
        time.sleep(1)
        print(f"progress: {i}")

# マルチスレッドの作成
thread1 = threading.Thread(target=print_numbers)

# 1秒おきに表示を行うスレッドの開始
thread1.start()

print("Step1")
time.sleep(random.randint(3, 10))
print("Step2")
time.sleep(random.randint(2, 10))
print("Step3")
time.sleep(random.randint(3, 10))

# メインスレッドの終了を待つ
thread1.join()

print("マルチスレッドの処理が完了しました。")

8-2, マルチプロセス

プロセスはそれぞれが独自のメモリ空間を持つ。
メモリを共有しづらいが、GILの影響を受けずに本当の並列処理が可能。
データを共有する場合は、プロセス間通信(IPC)を使用してデータをやりとりする必要がある。

from multiprocessing import Process
import time

def print_numbers(pid1):
    for i in range(5):
        time.sleep(1)
        print(f"Process {pid1}: {i}")

def print_letters(pid2):
    for chr in 'ABCDE':
        time.sleep(1)
        print(f"Process {pid2}: {chr}")

if __name__ == "__main__":
    # マルチプロセスの作成
    process1 = Process(target=print_numbers, args=(1,))
    process2 = Process(target=print_letters, args=(2,))

    # プロセスの開始
    process1.start()
    process2.start()

    # メインプロセスが待つ
    process1.join()
    process2.join()

    print("マルチプロセスの処理が完了しました。")

8-3. 非同期処理(asyncio)

非同期処理は、複数の処理(関数・メソッド)を非同期に動かすこと。
多重化でも並列処理でもないが、複数の処理を同時に動かすことができる。

import asyncio

async def print_numbers(pid):
    for i in range(5):
        await asyncio.sleep(1)
        print(f"Async Task: {pid}-{i}")

async def print_letters(pid):
    for letter in 'ABCDE':
        await asyncio.sleep(1)
        print(f"Async Task: {pid}-{letter}")

async def main():
    task1 = asyncio.create_task(print_numbers('A'))
    task2 = asyncio.create_task(print_numbers('B'))
    # task2 = asyncio.create_task(print_letters('B'))

    await asyncio.gather(task1, task2)

asyncio.run(main())
print("非同期処理が完了しました。")