スレッドの基本操作 | スレッドの作成 | Python本格超入門

スポンサーリンク
スポンサーリンク
amazon
スマイルSALE
--:--:--
~
ad. 価格範囲を指定して商品を探せます

スレッドの基本操作

Pythonでネットワーク通信の待ち時間を有効活用したり、複数の処理をバックグラウンドで同時に進めたりしたい場合、「並行処理」というアプローチが不可欠です。そのための最も基本的かつ軽量な仕組みが、threading(マルチスレッド)モジュールです。

スレッドとは、1つのプログラム(プロセス)の内部で実行される「処理の単位(糸)」のことです。メインの処理とは別に新しいスレッドを立ち上げることで、ユーザーインターフェースをフリーズさせずに重い通信処理を裏側で実行するといった制御が可能になります。

スレッドの作成と実行

Pythonで新しいスレッドを作成するには、標準ライブラリの threading.Thread クラスを使用します。関数をターゲットとして指定する最も基本的な実装例です。

import threading
import time

def print_numbers():
    """別スレッドで実行させたい関数"""
    for i in range(3):
        print(f"Worker(サブスレッド): {i}")
        time.sleep(1)  # 1秒待機

# 1. スレッドオブジェクトの作成(targetに実行したい関数名を指定)
# ※関数名に () を付けないよう注意してください。付けるとその場で実行されてしまいます。
task_thread = threading.Thread(target=print_numbers)

# 2. スレッドの開始(バックグラウンドで関数が走り出す)
task_thread.start()

# メインスレッド(本来の処理ライン)も同時に並行して進む
for i in range(3):
    print(f"Main(メインスレッド): {i}")
    time.sleep(0.5)  # 0.5秒待機

コードの詳細解説と重要ルール

上記のコードを実行すると、メインの処理とサブの処理が交互、あるいは同時にバラバラのタイミングで出力されます。

  1. target=print_numbers:スレッドに任せたい処理(関数名)を渡します。もし関数に引数を渡したい場合は、args=(引数1, 引数2) のようにタプル形式で別途指定します。
  2. start():スレッドを起動するトリガーです。このメソッドを呼び出した直後、プログラムは関数の終了を待たずに即座に次の行(メインのループ処理)へ進みます。これが並行処理の基本動作です。

スレッドの終了同期(join)の必要性

スレッドを立ち上げた際、メインスレッドが先に全処理を終えてしまうと、サブスレッドがまだ裏で動いている途中でプログラム全体が終了してしまったり、後続の集計処理に不具合が出たりすることがあります。

サブスレッドの処理が「完全に終わるまで次の行へ進まずに待つ」という同期制御を行うのが join() メソッドです。

import threading
import time

def download_file():
    print("ファイルのダウンロードを開始します...")
    time.sleep(3)  # 重い通信を模した3秒待機
    print("ダウンロードが完了しました!")

thread = threading.Thread(target=download_file)
thread.start()

print("メイン:別の作業をしながらダウンロードの完了を待ちます...")

# 【重要】このスレッドの処理が完了するまで、メインスレッドをここで一時停止させる
thread.join()

print("メイン:ダウンロードされたデータを使って、次の処理を開始します。")

スレッドの安全な停止方法(Eventオブジェクトの利用)

Pythonのマルチスレッドにおける重要な注意点として、「外部からスレッドを強制終了させるメソッドは用意されていない」という点が挙げられます。処理の途中で無理やりスレッドを破壊すると、共有しているデータが壊れたりメモリリークが発生したりするためです。

そのため、スレッドを安全に止めるには、threading.Event() を用いて「停止フラグ」を内部で監視する設計にするのがプロの標準的な作法です。

import threading
import time

class StoppableWorker(threading.Thread):
    def __init__(self):
        super().__init__()
        # スレッドの停止シグナルを管理するEventオブジェクト
        self._stop_event = threading.Event()

    def run(self):
        """start()が呼ばれると自動でこのrunメソッドが実行されます"""
        print("ループ処理を開始します。")
        # フラグがセットされる(Trueになる)まで無限ループ
        while not self._stop_event.is_set():
            print("裏側で定期処理を実行中...")
            time.sleep(1)
        print("安全にループを抜けました。スレッドを終了します。")

    def stop(self):
        """外部からこのメソッドを呼ぶことでフラグを立てる"""
        self._stop_event.set()

# スレッドの生成と起動
worker = StoppableWorker()
worker.start()

# 3秒間だけ働かせる
time.sleep(3)

print("メイン:作業停止を指示します。")
worker.stop()  # フラグをTrueにする
worker.join()  # 完全に止まるのを看取る
print("プログラムを終了しました。")

スレッドの状態確認(is_alive)

現在、立ち上げたスレッドがまだ元気に処理を継続しているのか、それともすでに終了しているのかをプログラム側で確認したい場合は、is_alive() メソッドで真偽値(True / False)を取得できます。

# スレッドの状態をチェックする実用パターン
if thread.is_alive():
    print("スレッドは現在もバックグラウンドで処理を実行中です。")
else:
    print("スレッドはすでに処理を完了、または停止しています。")

知っておくべき制限:GIL(グローバルインタプリタロック)の罠

マルチスレッドを扱う上で、絶対に知っておくべきPython特有の制限があります。それがGIL(Global Interpreter Lock)です。

Python(CPython)は安全性のために、「どれだけマルチスレッドを立ち上げても、同時にCPUのコアで実行できるスレッドは常に1つだけ」という厳格なロック機構を持っています。そのため、画像の加工や大量の計算といった「CPUに負荷がかかる重い処理」をマルチスレッド化しても、処理速度は一切速くなりません(むしろ切り替えのオーバーヘッドで遅くなります)。

マルチスレッドが真価を発揮するのは、Web APIの呼び出しやファイルの読み書き、スクレイピングなど、「CPUは暇だけど、通信や入出力の応答を待っている時間が長い処理(I/Oバウンドな処理)」を並行化するときです。もし計算を高速化したい場合は、メモリ空間を完全に分けて並列化する「マルチプロセス(multiprocessing)」を選択しましょう。

まとめ

  • start で非同期に走らせ、join で同期する:start() を呼ぶと別ラインで処理が即座に始まり、その完了を確実に待ち受けたい箇所で join() を呼び出すのがスレッドの基本サイクルです。
  • 強制終了はさせず、Eventで制御する:安全なアプリケーション設計のために、外部からの強制破壊は避け、threading.Event() のフラグ変更をループ内で検知させて自発的に終了させる実装を徹底します。
  • I/O待ちの効率化に使う:PythonにはGILの制限があるため、スレッドは「計算の高速化」ではなく、ネットワークやディスクの「待ち時間の有効活用(並行化)」を目的に導入するのが正しい選択です。