Pythonでマルチスレッドを制御する5つの方法

Pythonを使ったマルチスレッド制御のチュートリアルPython
この記事は約10分で読めます。

【サイト内のコードはご自由に個人利用・商用利用いただけます】

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

基本的な知識があればサンプルコードを活用して機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

プログラミング言語Pythonでは、マルチスレッドという仕組みを利用して、複数のタスクを同時に実行することが可能です。

今回は、そのようなマルチスレッドの制御を行うための5つの具体的な方法を初心者の方でも理解できるように解説していきます。

ここでは、Pythonにおけるマルチスレッドの基本から、具体的な制御方法、さらには注意点までを包括的に紹介します。

●Pythonとマルチスレッドの基本

Pythonでマルチスレッドを実装するためには、”threading”という組み込みモジュールを利用します。

マルチスレッドとは一言で言うと、一つのプロセス内で複数の処理を並行して実行するための仕組みのことを指します。

○Pythonにおけるマルチスレッドの利点と限界

マルチスレッドの最大の利点は、I/O待ち時間の短縮やCPUの効率的な利用を可能にすることです。

しかし、一方でPythonのマルチスレッドはGlobal Interpreter Lock(GIL)という仕組みにより、1つのスレッドしか同時に実行できないという制約があります。

このため、CPU密集型の処理をマルチスレッドで行おうとすると、期待する程のパフォーマンス向上は得られません。

○マルチスレッドとマルチプロセスの違い

Pythonにおけるマルチスレッドとマルチプロセスの最大の違いは、前者がメモリを共有するのに対して、後者は各プロセスが独立したメモリ空間を持つことです。

これにより、マルチプロセスはGILの制約を受けずに複数のCPUコアを活用できますが、プロセス間の通信には注意が必要となります。

●Pythonでマルチスレッドを制御するための5つの方法

それでは具体的なマルチスレッド制御の方法について見ていきましょう。

○方法1:threadingモジュールの基本的な使い方

Pythonでマルチスレッドを実装するための基本的な手法として、threadingモジュールを使用する方法があります。

このコードではthreadingモジュールを使って2つのスレッドを作成し、それぞれが並行して実行されることを確認します。

□サンプルコード1:threadingモジュールを使った基本的なマルチスレッドの実装

import threading
import time

def print_numbers():
    for i in range(10):
        time.sleep(1)
        print(i)

def print_chars():
    for char in 'abcdefghij':
        time.sleep(1)
        print(char)

thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_chars)

thread1.start()
thread2.start()

このコードでは、print_numbers関数とprint_chars関数をそれぞれ異なるスレッドで実行しています。

各関数は1秒ごとに数字または文字を出力します。

この例では、2つのスレッドが同時に開始され、それぞれが1秒ごとに異なるタスクを実行しています。

実行すると、数字と文字が交互に出力され、2つのスレッドが並行して動作していることが確認できます。

ここで注目すべきは、スレッドが作成されると、そのスレッドは親プロセスとは独立して実行されるという点です。

つまり、thread1.start()の次に直ちにthread2.start()が実行され、2つのスレッドが並行して動作を開始します。

○方法2:並列処理を行うスレッドの作成

次に、並列処理を行うためのスレッドを作成する方法を見てみましょう。

ここでは、リスト内の数値をそれぞれ平方にするという処理を並列に行ってみます。

□サンプルコード2:並列処理を行うスレッドの実装

import threading

numbers = [2, 3, 5, 6]

def square(n):
    print(f'The square of {n} is {n ** 2}')

threads = []
for num in numbers:
    thread = threading.Thread(target=square, args=(num,))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

このコードでは、リスト内の各数値に対して平方を計算する関数squareを並列に実行しています。

各スレッドはthreading.Threadで作成し、startメソッドで実行を開始します。

最後にjoinメソッドを使用して、全てのスレッドが終了するのを待っています。

この結果、4つのスレッドが並行に実行され、各数値の平方が計算されます。

○方法3:スレッド間でデータを共有する

次に、スレッド間でデータを共有する方法について見てみましょう。

スレッド間でデータを共有することで、複数のスレッドが共同で一つのタスクに取り組むことが可能となります。

この方法は特に、大量のデータを処理する場合や、複数のデバイスからの入力を管理する場合などに有用です。

□サンプルコード3:スレッド間のデータ共有の実装

import threading

# 共有データ
data = []

def append_data(n):
    for i in range(n):
        data.append(i)

# スレッドの作成と開始
thread1 = threading.Thread(target=append_data, args=(5,))
thread2 = threading.Thread(target=append_data, args=(5,))

thread1.start()
thread2.start()

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

# データの出力
print(data)

このコードでは、2つのスレッドが同時に共有のリストdataにデータを追加しています。

最後に全てのスレッドが終了した後にリストの中身を出力しています。

これにより、2つのスレッドが同時にデータを共有リストに追加した結果が確認できます。

このような形でスレッド間でデータを共有することは、大量のデータを効率良く処理するために有用です。

しかし、同時に複数のスレッドが同じデータにアクセスしようとするとデータの矛盾が生じる可能性があるため、注意が必要です。

○方法4:スレッドの同期

先ほどのデータ共有に関連して、スレッドの同期というテーマに触れます。

マルチスレッド環境では、複数のスレッドが同時に実行されるため、予期せぬ競合状態やデータの矛盾が発生する可能性があります。

これを防ぐためには、スレッドの同期を行い、特定のスレッドが一部のコードを実行している間、他のスレッドがそれを邪魔しないようにする必要があります。

□サンプルコード4:スレッドの同期の実装

import threading

lock = threading.Lock()
data = []

def append_data(n):
    for i in range(n):
        with lock:
            data.append(i)

thread1 = threading.Thread(target=append_data, args=(5,))
thread2 = threading.Thread(target=append_data, args=(5,))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

print(data)

このコードでは、threading.Lockを使用してスレッドの同期を取っています。

Lockオブジェクトは、あるスレッドがロックを獲得している間、他のスレッドが同じロックを獲得しようとするとブロックされます。

つまり、この例では、あるスレッドがdata.append(i)を実行している間、他のスレッドはその操作が終了するまで待たされ、データの競合が防がれます。

○方法5:スレッドプールを使った大量のタスクの管理

大量のタスクを効率よく処理するためには、スレッドプールという仕組みを利用すると良いでしょう。

スレッドプールとは、事前に作成したスレッド群をプール(プールは適切な数のスレッドを保持し、タスクが投げられると利用可能なスレッドにそのタスクを割り当てる)として保持し、タスクを効率よく処理するための仕組みです。

□サンプルコード5:スレッドプールを使ったタスク管理の実装

from concurrent.futures import ThreadPoolExecutor

def square(n):
    return n ** 2

numbers = [2, 3, 5, 6, 7, 8, 9, 10]

with ThreadPoolExecutor() as executor:
    results = executor.map(square, numbers)

for result in results:
    print(result)

このコードでは、concurrent.futuresモジュールのThreadPoolExecutorを使ってスレッドプールを作成しています。

そして、executor.mapを使って、各数値に対してsquare関数を並列に適用しています。

スレッドプールは大量のタスクを効率的に処理するのに役立ちますが、同時に多くのスレッドを生成するとシステムのリソースを過度に消費する可能性があるため、適切なスレッド数の設定が必要です。

●Pythonのマルチスレッド制御の注意点と対処法

以上のようにPythonでマルチスレッドを実装し制御する方法は多数存在しますが、マルチスレッドプログラミングには次のような注意点があります。

①GIL(Global Interpreter Lock)の影響

PythonのCPythonインタープリタは、一度に1つのスレッドしか実行できないGILという仕組みがあります。

これはCPU密集型のタスクにおいて、マルチスレッドが期待通りの高速化をもたらさない原因となります。

この問題を解決するには、マルチプロセスや非同期プログラミングを使用する、あるいはGILを回避できる実装(例えばJythonやIronPythonなど)を使用すると良いでしょう。

②データ競合

複数のスレッドが同時に同じデータにアクセスすると、データの不整合や競合が発生する可能性があります。

これを防ぐには、前述のスレッド同期のテクニックを利用すると良いでしょう。

③デッドロック

2つ以上のスレッドが互いに他方が放棄するまでロックを獲得できない状態に陥ると、デッドロックが発生します。

デッドロックを避けるためには、ロックの取得順序を統一したり、タイムアウトを設定するなどの工夫が必要です。

以上、Pythonでマルチスレッドを制御するための5つの方法と注意点について解説しました。

マルチスレッドをうまく活用することで、プログラムの性能を向上させ、より効率的なコードを書くことができます。

ただし、マルチスレッドプログラミングは複雑性が高く、データ競合やデッドロックなどの問題を引き起こす可能性がありますので、注意深く取り組むことが重要です。

また、可能な場合は、マルチプロセスや非同期プログラミングなど、他の並行処理の手法も検討してみてください。

まとめ

Pythonでのマルチスレッド制御は、一部のタスクを高速化し、プログラムの効率を向上させる強力な手段です。

本記事ではPythonでマルチスレッドを制御するための5つの具体的な方法とサンプルコードを通じて、その基本的な概念と実装方法を解説しました。

しかし、マルチスレッドプログラミングはその複雑性から、データ競合やデッドロックなどの問題を引き起こす可能性があります。

そのため、これらの問題を理解し、適切な手法で対処することが重要です。

これからPythonでマルチスレッド制御を行う方々が、本記事を参考に、より効果的で安全なマルチスレッドプログラミングが行えることを願っています。