threadingモジュールとは?
Python
のthreading
モジュールは、複数のスレッドを使ってプログラムの並行処理を実現するための標準ライブラリです。スレッドは、軽量なプロセスのようなもので、同じプロセス内で並行してタスクを実行します。これにより、プログラムの応答性を向上させたり、I/O待機時間を効率的に活用することが可能です。
ただし、Python
にはGIL(Global Interpreter Lock)という制約があり、特にCPUバウンドなタスクではスレッドのパフォーマンスが制限されることがあります。そのため、threading
モジュールは主にI/Oバウンドな処理に向いています。
この記事では、threading
モジュールを使った基本的なマルチスレッド処理の実装方法と、GILの影響、さらにスレッドを安全に管理する方法について詳しく解説します。
マルチスレッドとマルチプロセスの違い
まず、Python
の並列処理において、マルチスレッドとマルチプロセスの違いを理解することが重要です。
- マルチスレッド
同じプロセス内で複数のスレッドが並行して動作します。スレッドは同じメモリ空間を共有するため、スレッド間でのデータのやり取りが高速で効率的です。しかし、GILの影響により、同時に1つのスレッドしか実行されないため、CPUバウンドな処理には向いていません。 - マルチプロセス
各プロセスは独立したメモリ空間を持ち、並列に動作します。multiprocessing
モジュールはこの方式を提供し、GILの制約を回避してCPUバウンドな処理を効率的に並列化できます。
どちらを使うべきか?
- I/Oバウンドなタスク(ネットワーク、ファイルI/Oなど)にはマルチスレッドが適しており、GILの影響を受けにくいです。
- CPUバウンドなタスク(計算処理など)ではマルチプロセスを使ったほうがパフォーマンスが向上します。
threadingモジュールの基本的な使い方
threading
モジュールでは、Thread
クラスを使ってスレッドを作成し、それぞれに実行する関数を指定できます。次に、スレッドを使って並行処理を実行する基本的な方法を見てみましょう。
スレッドの作成と実行
以下は、複数のスレッドを生成して同時に実行する例です。
import threading
import time
def worker(num):
"""スレッドごとに実行される関数"""
print(f"Worker {num} started")
time.sleep(2)
print(f"Worker {num} finished")
# スレッドを作成して実行
threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
# すべてのスレッドが終了するのを待つ
for t in threads:
t.join()
print("すべてのスレッドが終了しました")
解説
threading.Thread
Thread
クラスは、新しいスレッドを生成します。target
引数にはスレッドで実行したい関数を指定し、args
でその関数に渡す引数を指定します。start()
スレッドのstart()
メソッドを呼び出すと、指定した関数が並行して実行されます。join()
join()
メソッドは、スレッドが終了するまで親スレッドが待機するためのメソッドです。これを使わないと、メインスレッドが他のスレッドの完了を待たずに終了してしまう可能性があります。 上記の例では、5つのスレッドが並行してworker
関数を実行します。各スレッドは2秒間スリープし、終了するとメッセージを出力します。join()
によって、すべてのスレッドが終了するまでメインスレッドが待機します。
スレッド間でのデータ共有とスレッドセーフ
スレッドは同じメモリ空間を共有するため、グローバル変数や共有リソースに同時にアクセスすることができます。しかし、これには競合状態(race condition)という問題があり、複数のスレッドが同じリソースを同時に変更しようとすると、予期しない動作が発生する可能性があります。
ロックの使用
競合状態を防ぐために、threading
モジュールではロック(Lock
)を使って、特定のコードブロックに対して1つのスレッドだけがアクセスできるように制御できます。
import threading
lock = threading.Lock()
counter = 0
def increment_counter():
global counter
for _ in range(100000):
with lock:
counter += 1
threads = []
for _ in range(5):
t = threading.Thread(target=increment_counter)
threads.append(t)
t.start()
for t in threads:
t.join()
print(f"Final counter: {counter}")
解説
- ロックの取得と解放
Lock
オブジェクトを使って、with lock:
の中の処理が排他制御されます。このブロック内では、同時に1つのスレッドだけがcounter
を変更できるため、競合状態を防げます。 - スレッドセーフな操作
ロックを使って共有リソースへのアクセスを制御することで、スレッドセーフな操作が可能になります。
デーモンスレッド
スレッドには通常のスレッドとデーモンスレッドがあります。デーモンスレッドは、メインスレッドが終了すると強制的に終了されます。バックグラウンドで実行したいタスクがある場合にデーモンスレッドを使うと便利です。
import threading
import time
def background_task():
while
True:
print("Background task running...")
time.sleep(1)
# デーモンスレッドを作成
t = threading.Thread(target=background_task)
t.daemon = True # デーモンスレッドに設定
t.start()
# メインスレッドが5秒後に終了
time.sleep(5)
print("Main thread finished")
解説
daemon=True
Thread
オブジェクトのdaemon
属性をTrue
に設定すると、そのスレッドはデーモンスレッドとして扱われます。メインスレッドが終了すると、デーモンスレッドも強制的に終了します。- バックグラウンド処理
デーモンスレッドは、バックグラウンドで定期的に実行されるタスクに適していますが、重要なタスクには適していません。メインスレッドが終了すると、デーモンスレッドも即座に終了してしまうためです。
threadingモジュールの便利な機能
threading
モジュールには、スレッド管理を支援するための便利なクラスや機能がいくつか用意されています。
ThreadPoolExecutor
concurrent.futures
モジュールに含まれるThreadPoolExecutor
は、スレッドプールを使って簡単にスレッドを管理できる高レベルのインターフェースです。
from concurrent.futures import ThreadPoolExecutor
def worker(num):
print(f"Worker {num} running")
with ThreadPoolExecutor(max_workers=3) as executor:
for i in range(5):
executor.submit(worker, i)
解説
- スレッドプール
ThreadPoolExecutor
は、指定した数のスレッドをプールし、複数のタスクを並行して実行します。max_workers
で同時に実行するスレッドの数を制御します。 submit()
メソッド
submit()
メソッドを使って、関数とその引数をスレッドプールに渡します。スレッドプール内でタスクが順次実行されます。
イベント(Event
)
スレッド間でシグナルを送るために、threading.Event
を使うことができます。Event
オブジェクトは、スレッドにフラグを設定して、あるスレッドが他のスレッドを待機させる際に利用されます。
import threading
import time
event = threading.Event()
def wait_for_event():
print("Waiting for event...")
event.wait() # イベントがセットされるまで待機
print("Event received!")
t = threading.Thread(target=wait_for_event)
t.start()
time.sleep(3)
print("Setting event")
event.set() # イベントをセット
解説
Event.wait()
wait()
メソッドを呼ぶと、イベントがセットされるまでスレッドが待機します。Event.set()
set()
メソッドでイベントをセットすると、wait()
していたスレッドが再開します。
マルチスレッディングの利点と限界
利点
-
I/Oバウンド処理に有効: マルチスレッドは、ファイル入出力やネットワーク通信などの待機時間が多い処理に最適です。これにより、スレッドが待機している間に他のスレッドが実行され、効率的な並行処理が可能です。
-
軽量な並行処理: スレッドはプロセスに比べてメモリ消費が少なく、プロセス間通信も不要なため、軽量な並行処理を実現します。
限界
-
GILの制約:
Python
のGILによって、CPUバウンドな処理ではマルチスレッドの効果が限定されます。GILは、Python
のメモリ管理をスレッドセーフにするために、同時に1つのスレッドしかPython
コードを実行できないという制約を課します。そのため、計算量が多いCPUバウンドな処理では、マルチスレッドのパフォーマンス向上は期待できません。 -
スレッド間の競合: スレッドは同じメモリ空間を共有するため、データの競合が発生しやすくなります。これを防ぐために、ロックやセマフォなどの同期機構を適切に使う必要があります。これが不適切に管理されると、デッドロックや競合状態の原因になります。
まとめ
Python
のthreading
モジュールを使えば、I/Oバウンドな処理を効率的に並行処理することが可能です。スレッドを使うことでプログラムの応答性を向上させ、I/O待機中にも他の作業を進めることができます。また、ロックやイベントなどの機構を使って、スレッド間のデータ共有を安全に管理することができます。
ただし、CPUバウンドな処理では、GILの制約によりマルチスレッドの効果が限られるため、multiprocessing
モジュールや他の並列処理技術を検討することが推奨されます。