Python PR

【Python】並行処理(マルチスレッドプログラミング)を行う方法

記事内に商品プロモーションを含む場合があります

この記事では、Pythonで並行処理を行う方法を解説します。

並行処理とは、複数のスレッドを高速で切り替えて実行することでいくつかの処理を並行して実行する処理方法です。

具体的にどのような場面で使われるかというと、ユーザー入力などの止まってしまう処理を実行しつつ、背後で他の処理を動かしたい場合などに使われます。

それでは、並行処理の方法を見ていきましょう!

並行処理を実装する

Pythonで並行処理するには、threadingモジュールを使います。このモジュールは元はオプションでしたが、Python 3.7からはインポートするだけで使用可能です。

import threading

threadingモジュールで並行処理する方法は2つあります。

1つ目は「呼び出し可能オブジェクトを渡す方法」で2つ目は「Threadクラスを継承してrun()メソッドをオーバーライドする方法」です。

方法1. 呼び出し可能オブジェクトを渡す

threading.Thread()クラスをインスタンス化する際、コンストラクタのtarget引数に呼び出し可能オブジェクトを渡します。

thread = threading.Thread(target=呼び出し可能オブジェクト)

すると、指定した呼び出し可能オブジェクトの処理を実行するスレッドオブジェクトを生成される。スレッドオブジェクトの活動を開始するにはstart()メソッドを呼び出す必要があります。

スレッドオブジェクト.start()

以下の例では、メインスレッドで0.6秒ごと、サブスレッドで1秒ごとに数字を出力しています。

import time, threading

# 1秒ごとにiを出力する関数
def func():
    for i in range(3):
        time.sleep(1)
        print(f"サブ: {i}")

# スレッドオブジェクトの生成
thread = threading.Thread(target=func)

# run()の起動
thread.start()

# メインスレッドの処理
for i in range(3):
    time.sleep(0.6)
    print(f"メイン: {i}")

実行結果

メイン: 0
サブ: 0
メイン: 1
メイン: 2
サブ: 1
サブ: 2

方法2. 継承 + オーバーライド

threading.Thread()クラスを継承したクラスを定義し、runメソッドに処理をオーバーライドする。そしたら、そのクラスをインスタンス化してstartメソッドを実行する。

start()メソッドはスレッドのrun()メソッドを起動します。

import threading

class MyThread(threading.Thread):

    def run(self):
        # スレッドで行わせたい処理

subthread = MyThread()
subthread.start()

__init__()とrun()メソッド以外はオーバーライドしてはいけないので注意してください。

以下のコードでは、メインスレッドで1秒間隔で現在時刻(秒のみ)を出力し、サブスレッドで2秒間隔で出力しています。

import threading
from datetime import datetime as dt
import time

# スレッドクラス
class MyThread(threading.Thread):

    # 活動の定義
    def run(self):
        for i in range(1, 4):
            time.sleep(2)
            print(f'サブスレッド {i}回目:  {dt.now().second}秒')

        print('サブスレッド終了!')


subthread = MyThread()
subthread.start()

for i in range(1, 4):
    time.sleep(1)
    print(f'メインスレッド {i}回目:  {dt.now().second}秒')

print('メインスレッド終了!')

実行結果

メインスレッド 1回目:  11秒
サブスレッド 1回目:  12秒
メインスレッド 2回目:  12秒
メインスレッド 3回目:  13秒
メインスレッド終了!
サブスレッド 2回目:  14秒
サブスレッド 3回目:  16秒
サブスレッド終了!



スレッド名の設定・変更・取得

threading.Thread()クラスのコンストラクタにname引数を指定すると任意の名前をスレッド名に設定できる。この名前は識別のためにのみ用いられる文字列です。

import threading

class MyThread(threading.Thread):

    def run(self):
        pass


subthread1 = MyThread(name="スレッド1")
print(subthread1.name) # スレッド1

subthread2 = threading.Thread(name="スレッド2")
print(subthread2.name) # スレッド2

デフォルトでは以下のように命名される。

  • Thread-N (Nは1から始まる10進数)
  • Thread-N(target) (targetはtarget.__name__)
import threading

class MyThread(threading.Thread):

    def run(self):
        pass


subthread1 = MyThread()
print(subthread1.name) # Thread-1

subthread2 = threading.Thread(target=print)
print(subthread2.name) # Thread-2 (print)

スレッド名を変更するにはnameプロパティに直接代入する。ゲッターやセッターが用意されていましたがPython 3.10で非推奨となった。

import threading

class MyThread(threading.Thread):

    def run(self):
        pass


subthread1 = MyThread(name="スレッド1")
print(subthread1.name) # スレッド1

subthread1.name = "T1"
print(subthread1.name) # T1

subthread2 = threading.Thread(name="スレッド2")
print(subthread2.name) # スレッド2

subthread1.name = "T2"
print(subthread1.name) # T2

引数を渡す

threading.Thread()クラスのコンストラクタにargs引数を指定すると対象となった呼び出し可能オブジェクトに引数を渡すことができます。

args引数は、リストかタプルで指定します。デフォルトは()

import threading

subthread = threading.Thread(target=print, args=(1, 2, 3))
subthread.start() # 1 2 3

キーワード引数で渡したい場合はkwargs引数に辞書で指定します。デフォルトは{}です。

import threading

subthread = threading.Thread(target=print, args=(1, 2, 3), kwargs={"sep": "/"})
subthread.start() # 1/2/3

もちろん、サブクラス内でも使用可能です。

import threading

class MyThread(threading.Thread):

    def run(self):
        print(self._args, self._kwargs)


subthread = MyThread(args=(1, 2, 3), kwargs={"sep": "/"})
subthread.start() # (1, 2, 3) {'sep': '/'}

デーモンスレッド

threading.Thread()クラスのコンストラクタにdaemon引数を指定することで生成されるスレッドオブジェクトがデーモンスレッドかどうか設定できます。

daemon引数はPython 3.3から追加されました。

デーモンスレッドは、残っているスレッドがデーモンスレッドだけになった時にPythonプログラム全体を終了させます。

import threading
import time


def worker():
    count = 1
    while True:
        time.sleep(1)
        print(f"サブスレッド: {count}")
        count += 1

sub = threading.Thread(target=worker, daemon=True)
sub.start()

time.sleep(5)
print("メインスレッド終了")

実行結果

サブスレッド: 1
サブスレッド: 2
サブスレッド: 3
サブスレッド: 4
メインスレッド終了

上記コードのサブスレッドは無限ループでしたがデーモンスレッドだったのでメインスレッドが終了した時にサブスレッドも終了しました。

スレッドが終わるまで待つ

任意のスレッドが終わるまで待つにはJoin()メソッドを使います。

join()メソッドが呼ばれたスレッドが終了(正常終了、例外で終了、オプションのタイムアウトが発生する)までメソッドの呼び出し元のスレッドをブロックします。

import threading
from datetime import datetime as dt
import time

# スレッドクラス
class MyThread(threading.Thread):

    # 活動の定義
    def run(self):
        for i in range(1, 4):
            time.sleep(2)
            print(f'サブスレッド {i}回目:  {dt.now().second}秒')

        print('サブスレッド終了!')


subthread = MyThread()
subthread.start()

# サブスレッドが終わるまで待つ
subthread.join()

for i in range(1, 4):
    time.sleep(1)
    print(f'メインスレッド {i}回目:  {dt.now().second}秒')

print('メインスレッド終了!')

実行結果

サブスレッド 1回目:  0秒
サブスレッド 2回目:  2秒
サブスレッド 3回目:  4秒
サブスレッド終了!
メインスレッド 1回目:  5秒
メインスレッド 2回目:  6秒
メインスレッド 3回目:  7秒
メインスレッド終了!

スレッドの計算結果を他のスレッドで使用したい場合などに使います。



まとめ

この記事では、Pythonの並行処理について解説しました。

ユーザー入力を使いたい時に裏で他の処理も走らせたいなら並行処理を使いましょう!

また並行処理はI/Oバウンド(ファイルの読み書きなど)を高速化することもできます。高速化については以下の記事が参考になります。

外部リンク【Pythonで高速化】I / Oバウンドとか並列処理とかマルチプロセスとかってなんぞや - Qiita

並行処理のデータ受け渡しにはQueue(キュー)を使うと便利です。

LinkQueue(キュー)の種類と使い方

それでは今回の内容はここまでです。ではまたどこかで〜( ・∀・)ノ