現代のコンピュータープログラムにおいて、複数のタスクを同時に実行する「並行処理」は、アプリケーションの応答性を高め、リソースを効率的に活用するために不可欠です。Pythonでは、この並行処理を実現するための主要な手段として「スレッド」が用いられます。
プロセスとスレッドの違い
並行処理を理解する上で、まず「プロセス」と「スレッド」の違いを明確にすることが重要です。
- プロセス (Process): オペレーティングシステムが管理する、プログラムの独立した実行単位です。それぞれが独自のメモリ空間、ファイルハンドル、その他リソースを持ち、互いに分離されています。プロセスの作成や切り替えには比較的大きなオーバーヘッドが伴います。
- スレッド (Thread): プロセス内で動作する、より軽量な実行単位です。一つのプロセス内に複数のスレッドが存在し、それらは親プロセスのリソース(メモリ空間など)を共有します。スレッドの作成や切り替えはプロセスに比べて高速ですが、リソースを共有するため、データ競合などの同期問題が発生する可能性があります。
Pythonにおけるスレッドは、主にI/Oバウンドなタスク(ネットワーク通信、ディスクI/O、ユーザー入力の待機など)のパフォーマンス向上に特に有効です。CPUバウンドなタスク(純粋な計算処理)の場合、PythonのGlobal Interpreter Lock (GIL) の存在により、真の並列実行は難しいですが、I/Oバウンドな処理ではスレッドがブロックされている間に他のスレッドが実行されるため、アプリケーション全体の応答性が向上します。
`_thread`モジュールによるスレッドの利用
Pythonでスレッドを作成する最も基本的な方法の一つは、低レベルなAPIを提供する`_thread`モジュールを使用することです。これはシンプルな関数呼び出しで新しいスレッドを開始できます。
import _thread
import time
# 各スレッドで実行される関数
def show_current_time(thread_identifier, interval_seconds):
count = 0
while count < 3:
time.sleep(interval_seconds)
current_time_str = time.ctime(time.time())
print(f"{thread_identifier}: 現在時刻 {current_time_str}")
count += 1
# 2つの異なるスレッドを開始
try:
_thread.start_new_thread(show_current_time, ("ワーカースレッド-A", 2))
_thread.start_new_thread(show_current_time, ("ワーカースレッド-B", 4))
except Exception as e:
print(f"エラーが発生しました: スレッドを開始できません。{e}")
# メインスレッドが直ちに終了しないように待機
time.sleep(15)
print("メインアプリケーションが終了しました。")
`_thread.start_new_thread(function, args[, kwargs])` は、指定された関数を新しいスレッドで実行します。`args`はタプル形式で、`kwargs`は辞書形式のオプション引数です。
`threading`モジュールによるスレッドの管理
より高レベルでオブジェクト指向的なインターフェースを提供し、より柔軟なスレッド管理を可能にするのが`threading`モジュールです。Pythonでスレッドを利用する際には、通常こちらのモジュールが推奨されます。
`threading.Thread`のサブクラス化
カスタムスレッドを作成する最も一般的な方法は、`threading.Thread`クラスを継承し、`run()`メソッドをオーバーライドすることです。`run()`メソッドには、スレッドが開始されたときに実行したいロジックを記述します。
import threading
import time
class TaskProcessor(threading.Thread):
def __init__(self, processor_name, num_tasks):
super().__init__() # 親クラスのコンストラクタを呼び出す
self.processor_name = processor_name
self.num_tasks = num_tasks
def run(self):
print(f"[{self.processor_name}] 処理を開始します。")
for i in range(self.num_tasks):
time.sleep(0.7) # タスク処理をシミュレート
print(f"[{self.processor_name}] タスク {i+1}/{self.num_tasks} を実行中...")
print(f"[{self.processor_name}] 処理を完了しました。")
# 2つのプロセッサスレッドを起動
processor_alpha = TaskProcessor("プロセッサ・アルファ", 3)
processor_beta = TaskProcessor("プロセッサ・ベータ", 4)
processor_alpha.start() # スレッドの実行を開始(run() メソッドが呼び出される)
processor_beta.start()
# 全てのスレッドが終了するのを待つ
processor_alpha.join()
processor_beta.join()
print("全てのタスクプロセッサが完了しました。")
`threading`モジュールの主要機能
- `threading.current_thread()`: 現在実行中のスレッドオブジェクトを返します。
- `threading.enumerate()`: 現在アクティブな全スレッドのリストを返します。
- `threading.active_count()`: 現在アクティブなスレッドの数を返します (`len(threading.enumerate())` と同じ)。
`Thread`インスタンスのメソッド
`threading.Thread`オブジェクトは、スレッドのライフサイクルを管理するための様々なメソッドを提供します。
- `start()`: スレッドの実行を開始します。これにより`run()`メソッドが呼び出されます。
- `join([timeout])`: 対象のスレッドが終了するまで、呼び出し元のスレッドをブロックします。`timeout`を指定すると、その秒数経過後にブロックが解除されます。
- `is_alive()`: スレッドが実行中である場合に`True`を返します。
- `name`: スレッドの名前を取得・設定するプロパティです(旧APIでは`getName()`、`setName()`)。
スレッド同期と競合状態
複数のスレッドが共有データに同時にアクセスし、変更を加える場合、意図しない結果(競合状態、またはレースコンディション)が発生する可能性があります。これを防ぐために「スレッド同期」の仕組みが必要です。
ロック (`threading.Lock`)
最も一般的な同期メカニズムの一つが「ロック」です。`threading.Lock`オブジェクトを使用すると、一度に一つのスレッドのみが特定のコードセクション(クリティカルセクション)を実行するように制限できます。
import threading
import time
shared_data = 0
data_lock = threading.Lock() # ロックオブジェクトを作成
# ロックを使用して共有データを安全にインクリメントする関数
def safe_increment_worker(thread_id, num_increments):
global shared_data
print(f"スレッド {thread_id} が開始します (安全)。")
for _ in range(num_increments):
data_lock.acquire() # ロックを取得
try:
# クリティカルセクション: ここは一度に一つのスレッドしか実行できない
current_value = shared_data
time.sleep(0.0001) # 競合状態を顕在化させるための意図的な遅延
shared_data = current_value + 1
finally:
data_lock.release() # ロックを解放 (finallyブロックで確実に解放することが重要)
print(f"スレッド {thread_id} が終了します (安全)。")
# ロックを使用せずに共有データをインクリメントする関数 (競合状態が発生する可能性あり)
def unsafe_increment_worker(thread_id, num_increments):
global shared_data
print(f"スレッド {thread_id} が開始します (危険)。")
for _ in range(num_increments):
# ロックなし
current_value = shared_data
time.sleep(0.0001) # 競合状態を顕在化させるための意図的な遅延
shared_data = current_value + 1
print(f"スレッド {thread_id} が終了します (危険)。")
# --- ロックを使用した安全な実行のデモンストレーション ---
print("\n=== ロック使用: 安全なインクリメント ===")
shared_data = 0 # 共有データをリセット
threads_safe = []
for i in range(5):
thread = threading.Thread(target=safe_increment_worker, args=(f"SAFE-{i+1}", 2000))
threads_safe.append(thread)
thread.start()
for t in threads_safe:
t.join()
print(f"安全な方法での最終的な共有データ値: {shared_data} (期待値: 10000)\n")
# --- ロックを使用しない危険な実行のデモンストレーション ---
print("=== ロック不使用: 危険なインクリメント ===")
shared_data = 0 # 共有データをリセット
threads_unsafe = []
for i in range(5):
thread = threading.Thread(target=unsafe_increment_worker, args=(f"UNSAFE-{i+1}", 2000))
threads_unsafe.append(thread)
thread.start()
for t in threads_unsafe:
t.join()
print(f"危険な方法での最終的な共有データ値: {shared_data} (期待値: 10000)")
上記の例では、ロックを使用しない場合、複数のスレッドが同時に`shared_data`を読み書きしようとすることで、最終結果が期待値よりも少なくなる「更新の紛失」が発生する可能性があります。`acquire()`と`release()`でクリティカルセクションを保護することで、この問題を回避できます。
`queue`モジュールによるスレッド間通信
スレッド間でデータを安全に受け渡すための、より洗練された方法として、Pythonの標準ライブラリ`queue`モジュールが提供する「スレッドセーフなキュー」を利用することが推奨されます。このモジュールには、以下の種類のキューが含まれています。
- `queue.Queue`: 先入れ先出し (FIFO) のキュー。
- `queue.LifoQueue`: 後入れ先出し (LIFO) のキュー。
- `queue.PriorityQueue`: 優先度に応じた順序でアイテムを取り出すキュー。
これらのキューは、内部的にロック機構を備えているため、複数のスレッドが同時にキューに対してデータを追加したり取り出したりしても、データ破損の心配がありません。プロデューサー・コンシューマーモデルなど、スレッド間で明確なデータフローがある場合に特に有効です。