Pythonで劇的に効率化!マルチスレッドプログラミング入門の10ステップ

Pythonとマルチスレッドのロゴと共に、初心者でも学べる10ステップを宣伝するテキストPython
この記事は約16分で読めます。

 

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

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

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

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

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

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

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

はじめに

Pythonの世界にようこそ。

今回はマルチスレッドプログラミングの基礎から学んでいきましょう。

マルチスレッドプログラミングとは一見難しそうな概念ですが、今回の記事を通じて10ステップでしっかりと理解し、実際にPythonでのマルチスレッドプログラミングに取り組むことができるようになることを目指しています。

●Pythonとマルチスレッドプログラミングとは

Pythonは読みやすさが特徴のプログラミング言語で、初心者にも取り組みやすい言語として知られています。

一方で、Pythonは高度な機能を持つプロフェッショナル向けの言語でもあります。

その一つがマルチスレッドプログラミングです。

●スレッドの基本概念

○スレッドとは

スレッドとは、プログラム内で同時に実行できる最小の処理単位を指します。

単一のプログラム内で複数のスレッドを作成し、それぞれが独立して動作することが可能です。

これにより、複数の処理を並行に行うことが可能となります。

○マルチスレッドとは

マルチスレッドとは、1つのプログラム内で複数のスレッドを同時に動作させることを指します。

これにより、複数の処理を同時並行で実行することが可能となり、処理の効率化やレスポンスの向上を図ることができます。

●Pythonにおけるマルチスレッドの取り扱い

○Pythonでのスレッドの作成

Pythonでスレッドを作成するには、標準ライブラリのthreadingモジュールを使用します。

スレッドを作成するためには、次のようにThreadクラスをインスタンス化します。

import threading

def thread_function():
    print("Hello, World!")

thread = threading.Thread(target=thread_function)

このコードでは、まずthreadingモジュールをインポートしています。

次に、スレッドで実行したい関数thread_functionを定義しています。この関数は”Hello, World!”と表示するだけの簡単なものです。

最後に、Threadクラスのインスタンスを作成しています。このとき、target引数にスレッドで実行したい関数を指定します。

○Pythonでのマルチスレッドの実装

作成したスレッドを実際に動作させるには、Threadクラスのstartメソッドを呼び出します。

thread.start()

このコードを実行すると、先程作成したスレッドが開始され、thread_function関数が実行されます。”Hello, World!”と表示されるはずです。

なお、スレッドの動作は非同期で行われ、startメソッドを呼び出した直後に次の行のコードが実行されます。

●マルチスレッドプログラミングの利点と制約

マルチスレッドプログラミングは、並列処理を活用しプログラムの効率を向上させることが可能な一方で、注意が必要な点も存在します。

ここでは、それぞれの特性を具体的に探ります。

まず、マルチスレッドプログラミングの最大の利点は、CPUのコアを効率的に使用して、同時に複数のタスクを並行して実行できることです。

このことにより、タスクが適切に分散されるため、全体のプログラムの実行時間が大幅に短縮されます。

さらに、ユーザーインターフェース(UI)があるアプリケーションでは、一部のタスクをバックグラウンドで実行しながら、UIをレスポンシブに保つことが可能です。

これにより、ユーザーはアプリケーションのレスポンスが遅いと感じることなく、快適に使用できます。

しかし、マルチスレッドプログラミングには、いくつかの制約もあります。

スレッドはメモリを共有して動作するため、データの同期を管理する必要があります。

スレッド間でデータが不適切に共有されると、「レースコンディション」という問題が生じ、プログラムが予期せぬ動作を示す可能性があります。

また、多くのスレッドを同時に扱うと、スレッドの管理やコンテキストスイッチによりシステムのオーバーヘッドが増大し、逆にパフォーマンスが落ちてしまう場合もあります。

したがって、スレッドの数は適切に管理することが重要です。

●Pythonでのマルチスレッドプログラミングの手順

では、実際にPythonでマルチスレッドプログラミングを行う手順を見ていきましょう。

初心者でも理解できるように、10ステップに分けて説明します。

○ステップ1:必要なモジュールのインポート

Pythonでマルチスレッドプログラミングを行うためには、threadingモジュールを使用します。

このモジュールはPythonに標準で組み込まれているため、追加でインストールする必要はありません。

threadingモジュールをインポートするコードを紹介します。

import threading

このコードでは、threadingモジュールをインポートしています。

これにより、この後のコードでスレッドの作成や操作が可能になります。

○ステップ2:スレッドを作成する関数の定義

次に、新しいスレッドで実行される関数を定義します。

この関数は、スレッドが開始されると自動的に実行されます。

下記のコードでは、’Hello, World!’を表示する関数を定義しています。

def print_hello():
    print('Hello, World!')

このコードでは、print_helloという関数を定義し、その中で’Hello, World!’と表示する処理を実装しています。

この関数が、新しいスレッドで実行されることになります。

○ステップ3:スレッドのインスタンスの生成

続いて、Pythonでスレッドのインスタンスを生成する方法を学んでいきましょう。

スレッドインスタンスの生成は、先ほど作成した関数を引数としてthreading.Threadクラスを呼び出すことで行います。

このコードでは、”print_numbers”関数と”print_time”関数の二つを使用してスレッドを作成します。

import threading
import time

def print_numbers():
    # 数字を出力するスレッド
    for i in range(10):
        time.sleep(1)
        print(i)

def print_time():
    # 時間を出力するスレッド
    for _ in range(10):
        print(time.ctime())
        time.sleep(2)

# スレッドのインスタンス生成
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_time)

この例では、”thread1″と”thread2″という二つのスレッドを作成しています。

“target”という引数にはスレッドとして実行したい関数を指定します。

“thread1″は数字を出力し、”thread2″は現在の時間を出力します。

スレッドの作成はとてもシンプルで、スレッドとして実行したい関数を指定するだけです。

このコードを実行すると、”thread1″と”thread2″がほぼ同時に実行されます。

しかし、まだスレッドは開始されていません。これは次のステップで行います。

○ステップ4:スレッドの開始

スレッドのインスタンスを生成したら、次にそのスレッドを開始します。

スレッドの開始は、生成したスレッドインスタンスのstartメソッドを呼び出すことで行います。

# スレッドの開始
thread1.start()
thread2.start()

この例では、”thread1.start()”と”thread2.start()”でスレッドが開始されます。

これにより、”print_numbers”関数と”print_time”関数が並列に実行されます。

このコードを実行すると、”thread1″が1秒ごとに0から9までの数字を出力し、”thread2″が2秒ごとに現在の時間を出力します。

これらのスレッドは互いに影響を与えずに独立して動作します。

しかし、このままではプログラムがすぐに終了してしまい、スレッドが完全に実行されない可能性があります。スレッドが全て終了するまでプログラムを待機させるために、スレッドの結合を行います。

これが次のステップです。

○ステップ5:スレッドの終了

スレッドの終了は自動的に行われます。

つまり、スレッドで実行される関数が終了すると、そのスレッドも自動的に終了します。

したがって、このステップで特別なコードを記述する必要はありません。

ただし、スレッドが終了するまで主プログラムを待機させるために、次のステップであるスレッドの結合が必要となります。

○ステップ6:スレッドの結合

スレッドが全て終了するまでメインのプログラムを待機させるためには、スレッドの結合を行います。これは、生成したスレッドインスタンスのjoinメソッドを呼び出すことで行います。

# スレッドの結合
thread1.join()
thread2.join()

この例では、”thread1.join()”と”thread2.join()”でスレッドの結合を行っています。

これにより、”thread1″と”thread2″が完全に終了するまでメインのプログラムが待機することになります。

このコードを実行すると、”thread1″が1秒ごとに0から9までの数字を出力し終わり、”thread2″が2秒ごとに現在の時間を出力し終わるまでメインのプログラムは終了しません。

これにより、全てのスレッドが完全に実行されることを確実にします。

○ステップ7:スレッドの状態の取得

スレッドの状態を知るには、生成したスレッドインスタンスのis_aliveメソッドを使用します。

このメソッドは、スレッドがまだ実行中であればTrueを、そうでなければFalseを返します。

# スレッドの状態の取得
print(thread1.is_alive())
print(thread2.is_alive())

この例では、”thread1.is_alive()”と”thread2.is_alive()”で各スレッドの状態を取得し、結果を出力しています。

このコードを実行すると、”thread1″と”thread2″が実行中であればTrueを、そうでなければFalseを出力します。

これにより、特定のスレッドがまだ実行中かどうかを確認することができます。

○ステップ8:スレッド間のデータ共有

スレッド間でデータを共有するためには、グローバル変数を使用します。

ただし、同時に複数のスレッドから同じデータにアクセスされると、データの不整合が生じる可能性があります。

これを防ぐためには、次のステップで説明するロックと同期が必要となります。以下に、スレッド間でデータを共有する例を表します。

import threading

# データ共有用のグローバル変数
counter = 0

def count_up():
    global counter
    for _ in range(1000000):
        counter += 1

# スレッドのインスタンス生成
thread1 = threading.Thread(target=count_up)
thread2 = threading.Thread(target=count_up)

# スレッドの開始
thread1.start()
thread2.start()

# スレッドの結合
thread1.join()
thread2.join()

# データの出力
print(counter)

この例では、”counter”というグローバル変数を用意し、2つのスレッドがこの変数を共有しています。

各スレッドは、”count_up”関数を用いて”counter”を1,000,000回増加させます。

ただし、このコードではロックを使用していないため、期待される結果とは異なる可能性があります。

○ステップ9:ロックと同期

スレッド間でのデータ共有において、同時に複数のスレッドから同じデータにアクセスされるとデータの不整合が生じる可能性があります。

これを防ぐために、一度に一つのスレッドのみがデータにアクセスできるようにするロックと呼ばれる仕組みを用います。

ロックは、PythonのthreadingモジュールのLockクラスを用いて実装します。

ロックを取得するには、Lockインスタンスのacquireメソッドを使用し、ロックを解放するにはreleaseメソッドを使用します。

ロックを用いてスレッド間でデータを共有する例を紹介します。

import threading

# データ共有用のグローバル変数とロックのインスタンス
counter = 0
counter_lock = threading.Lock()

def count_up():
    global counter
    for _ in range(1000000):
        with counter_lock:
            counter += 1

# スレッドのインスタンス生成
thread1 = threading.Thread(target=count_up)
thread2 = threading.Thread(target=count_up)

# スレッドの開始
thread1.start()
thread2.start()

# スレッドの結合
thread1.join()
thread2.join()

# データの出力
print(counter)

この例では、”counter”というグローバル変数と、それを保護するロック”counter_lock”を用意しています。

“with counter_lock:”の部分でロックを取得し、ブロックを抜けるときに自動的にロックが解放されます。

これにより、”counter += 1″が実行されるときには常に一つのスレッドのみが”counter”にアクセスすることが保証されます。

このコードを実行すると、”counter”は必ず2,000,000となります。

これは、各スレッドが”counter”を1,000,000回増加させるためです。

このように、ロックを用いることでデータの不整合を防ぎながら、スレッド間でのデータ共有を行うことが可能となります。

○ステップ10:エラーハンドリング

スレッドの中で例外が発生した場合、そのスレッドは静かに終了し、メインのプログラムは続行されます。

そのため、スレッドの中で例外を適切に処理するためには、スレッド関数の中でtry-except文を使用して例外を捕捉する必要があります。

import threading

def thread_func():
    try:
        # 何らかの処理(ここでは0での除算で例外を発生させる)
        result = 1 / 0
    except Exception as e:
        print(f"エラーが発生しました:{e}")

# スレッドのインスタンス生成
thread = threading.Thread(target=thread_func)

# スレッドの開始
thread.start()

# スレッドの結合
thread.join()

この例では、”thread_func”関数の中でtry-except文を使用して、例外を捕捉し、エラーメッセージを出力しています。

“1 / 0″は0での除算を試みるため、ZeroDivisionErrorが発生します。

このコードを実行すると、エラーメッセージが出力され、メインのプログラムは正常に終了します。

これにより、スレッドの中で発生した例外を適切に処理することができます。

●注意点と対処法

マルチスレッドプログラミングには多数の利点がありますが、注意しなければならないポイントもあります。

1つ目は、「グローバルインタープリタロック(GIL)」というPythonの仕様です。GILは一度に一つのスレッドしか実行できないという制限をもたらします。

そのため、CPUバウンドな処理を並列化すると、逆にパフォーマンスが低下することもあります。

この問題を解決する方法として、マルチプロセスの使用があります。

Pythonのmultiprocessingモジュールは各プロセスに対して独立したPythonインタープリタを割り当てるため、GILの制限を受けません。

from multiprocessing import Process

def count_up():
    count = 0
    for _ in range(10000000):
        count += 1

# プロセスのインスタンス生成
process1 = Process(target=count_up)
process2 = Process(target=count_up)

# プロセスの開始
process1.start()
process2.start()

# プロセスの結合
process1.join()
process2.join()

このコードでは、2つのプロセスを同時に実行し、それぞれでカウントアップを行っています。

プロセスは独立して動作するため、GILの影響を受けず、CPUをフルに活用することができます。

2つ目の注意点は、ロックの適切な使用です。

ロックは、リソースへの同時アクセスを防ぐために必要ですが、ロックが不適切に使用されるとデッドロックと呼ばれる問題を引き起こす可能性があります。

これを避けるためには、必要な範囲だけロックを行い、使用後はすぐにロックを解放するようにすることが重要です。

●応用例:Pythonでのマルチスレッドプログラミング

Pythonのマルチスレッドは、IOバウンドな処理の並列化において特に有用です。

例えば、Webサイトからのデータのスクレイピングなどでは、レスポンスを待っている時間が主となるため、マルチスレッドを使用すると劇的にパフォーマンスを向上させることが可能です。

Pythonのrequestsライブラリとthreadingモジュールを用いて、複数のWebサイトからデータを並列に取得する例を紹介します。

import requests
import threading

# 対象のURLリスト
urls = ["https://www.example.com", "https://www.example2.com", ...]

def fetch(url):
    response = requests.get(url)
    print(f"{url}: {response.status_code}")

# スレッドリストの作成
threads = []
for url in urls:
    thread = threading.Thread(target=fetch, args=(url,))
    threads.append(thread)

# スレッドの開始
for thread in threads:
    thread.start()

# スレッドの結合
for thread in threads:
    thread.join()

この例では、各スレッドが異なるWebサイトからデータを取得しています。

これにより、複数のWebサイトから同時にデータを取得することが可能となり、全体の実行時間を大幅に短縮することができます。

このように、Pythonのマルチスレッドは、適切に利用することで様々なタスクの効率化を実現できます。

まとめ

Pythonでマルチスレッドプログラミングを活用することで、タスクの並列化による効率化を実現できます。

しかし、注意すべき点や問題点も存在するため、適切に理解して使用することが重要です。

この記事で提供した10のステップを参考に、マルチスレッドプログラミングの理解を深め、Pythonのパワフルな並列処理能力を存分に利用しましょう。