マルチスレッド Python 例: GILを学ぶ Python

⚡ スマートサマリー

マルチスレッド Python 1つのプロセス内で複数のスレッドを実行することで、スレッドはメモリを共有し、並行して動作します。スレッドモジュールはこれらのスレッドを作成および管理しますが、グローバルインタープリタロックによって真の並列処理が制限されるため、この手法は入出力バウンドなタスクに最適です。

  • 🔘 スレッドとプロセス: スレッドとは、プロセス内部における実行単位であり、スレッドは個別に実行されながらもプロセスデータを共有する。
  • ☑️ 2つのモジュール: 廃止された_threadモジュールは低レベルの呼び出しを提供する一方、より高レベルのthreadingモジュールは、スレッドを構築するための現代の標準となっています。
  • スレッドを作成する: Threadクラスを拡張し、__init__とrun()をオーバーライドして、start()を呼び出して起動し、join()を呼び出して完了を待機します。
  • 🧪 レースコンディションを避ける: ロックを使用すると、一度に1つのスレッドのみがクリティカルセクションに入ることができるため、デッドロックや共有データの破損を防ぐことができます。
  • 🛠️ GILを理解する: グローバルインタープリタロックにより、1 つのスレッドが実行可能になります。 Python 一度に処理できるバイトコード量が少ないため、CPU負荷の高い処理にはマルチプロセッシングが必要となる。
  • 🤖 AI ワークロード: マルチスレッドは、データ読み込みなどの入出力バウンドな AI ステップを高速化しますが、NumPy と PyTorch 並列計算のためにGILを解放します。

マルチスレッド Python

その Python プログラミング言語では、マルチプロセッシングまたはマルチスレッドを使用できます。このチュートリアルでは、マルチスレッドアプリケーションを作成する方法を学びます。 Python.

スレッドとは

スレッドとは、並行プログラミングにおける実行単位のことです。マルチスレッドとは、CPUが1つのプロセスの複数のタスクを同時に実行できるようにする技術です。これらのスレッドは、プロセスリソースを共有しながら個別に実行できます。

プロセスとは

プロセスとは基本的に実行中のプログラムのことです。コンピュータでアプリケーション(ブラウザやテキストエディタなど)を起動すると、オペレーティングシステムがプロセスを作成します。 プロセス。

マルチスレッドとは Python?

マルチスレッド Python マルチスレッドプログラミングは、プロセス内の複数のスレッドがメインスレッドとデータ空間を共有する、よく知られた手法です。これにより、スレッド間の情報共有と通信が容易かつ効率的になります。スレッドはプロセスよりも軽量です。複数のスレッドは、プロセスリソースを共有しながら個別に実行できます。マルチスレッドの目的は、複数のタスクや関数を同時に実行することです。

マルチプロセッシングとは何ですか?

マルチプロセッシング 複数の無関係なプロセスを同時に実行できます。これらのプロセスはリソースを共有せず、IPC を介して通信します。

Python マルチスレッドとマルチプロセッシング

プロセスとスレッドを理解するために、次のシナリオを考えてみましょう。コンピュータ上の.exeファイルはプログラムです。それを開くと、OSがメモリに読み込み、CPUがそれを実行します。現在実行されているプログラムのインスタンスをプロセスと呼びます。

すべてのプロセスには、2つの基本的な構成要素があります。

  • その Code
  • データ

現在、プロセスには、と呼ばれる XNUMX つ以上のサブパートを含めることができます。 スレッド。 これはOSのアーキテクチャに依存します。スレッドとは、オペレーティングシステムによって個別に実行できるプロセスの一部と考えることができます。

言い換えれば、それはOSによって独立して実行できる命令のストリームです。単一プロセス内のスレッドは、そのプロセスのデータを共有し、並列処理を促進するために連携して動作するように設計されています。

マルチスレッドを使用する理由

マルチスレッドを使用すると、アプリケーションを複数のサブタスクに分割し、これらのタスクを同時に実行できます。マルチスレッドを適切に使用すると、アプリケーションの速度、パフォーマンス、レンダリングがすべて向上します。

Python マルチスレッド

Python マルチプロセッシングとマルチスレッドの両方の構造をサポートしています。このチュートリアルでは、主に実装に焦点を当てます。 マルチスレッド を使用したアプリケーション Pythonスレッドを処理するために使用できる主なモジュールは 2 つあります。 Python:

  1. その モジュール、および
  2. その スレッディング モジュール

しかし、 Pythonまた、グローバルインタプリタロック(GIL)と呼ばれるものもあります。これはパフォーマンスの向上にはほとんど役立たず、場合によっては 減らします 一部のマルチスレッド アプリケーションのパフォーマンス。 このチュートリアルの次のセクションですべてを学習します。

Thread モジュールと Threading モジュール

このチュートリアルで学習する XNUMX つのモジュールは、 スレッドモジュール スレッドモジュール.

しかし、スレッドモジュールは長い間廃止されてきました。 Python 3、廃止と指定されており、 _糸 下位互換性のため。

より高いレベルのものを使用する必要があります スレッディング デプロイ予定のアプリケーション用のモジュールです。スレッドモジュールについては、ここでは教育目的でのみ説明しています。

スレッドモジュール

このモジュールを使用して新しいスレッドを作成する構文は次のとおりです。

thread.start_new_thread(function_name, arguments)

さて、コーディングを開始するための基本理論を説明しました。 それで、あなたの IDLE またはメモ帳に次のように入力します。

import time
import _thread

def thread_test(name, wait):
   i = 0
   while i <= 3:
      time.sleep(wait)
      print("Running %s\n" %name)
      i = i + 1

   print("%s has finished execution" %name)

if __name__ == "__main__":
    
    _thread.start_new_thread(thread_test, ("First Thread", 1))
    _thread.start_new_thread(thread_test, ("Second Thread", 2))
    _thread.start_new_thread(thread_test, ("Third Thread", 3))

ファイルを保存し、F5 キーを押してプログラムを実行します。 すべてが正しく行われた場合、次のような出力が表示されます。

スレッドモジュール

今後のセクションでは、レース状況とその対処法について詳しく解説します。

スレッドモジュール

コードの説明

  1. これらのステートメントは、実行と遅延を処理するために使用されるtimeおよびthreadモジュールをインポートします。 Python スレッド。
  2. ここでは、という関数を定義しました。 スレッドテスト、 によって呼び出されます 新しいスレッドの開始 メソッド。この関数は、whileループを4回繰り返し実行し、呼び出し元のスレッド名を表示します。ループが完了すると、スレッドの実行が終了したことを示すメッセージを表示します。
  3. これはプログラムのメインセクションです。 ここでは、単に 新しいスレッドの開始 メソッドと スレッドテスト 関数を引数として指定します。これにより、引数として渡された関数用の新しいスレッドが作成され、実行が開始されます。なお、この関数(thread_test)は、スレッドとして実行したい他の関数に置き換えることができます。

スレッディングモジュール

このモジュールは、スレッド処理の高レベル実装です。 Python そして、マルチスレッドアプリケーションを管理するための事実上の標準となっています。スレッドモジュールと比較して、幅広い機能を提供します。

Threadingモジュールの構造

Threadingモジュールの構造

このモジュールで定義されているいくつかの便利な関数のリストを次に示します。

関数名 詳細説明
アクティブカウント() の数を返します スレッド まだ生きている物体。
currentThread() Thread クラスの現在のオブジェクトを返します。
enumerate() すべてのアクティブな Thread オブジェクトをリストします。
isDaemon() スレッドがデーモンの場合は true を返します。
生きている() スレッドがまだ生きている場合は true を返します。
スレッドクラスのメソッド
開始() スレッドのアクティビティを開始します。 複数回呼び出すと実行時エラーがスローされるため、スレッドごとに XNUMX 回だけ呼び出す必要があります。
run() このメソッドはスレッドのアクティビティを示し、Thread クラスを拡張するクラスによってオーバーライドできます。
join() join() メソッドが呼び出されたスレッドが終了するまで、他のコードの実行をブロックします。

バックストーリー: スレッド クラス

スレッドモジュールを使用してマルチスレッドプログラムのコーディングを開始する前に、Thread クラスを理解することが重要です。Thread クラスは、スレッドのテンプレートと操作を定義する主要なクラスです。 Python.

マルチスレッドを作成する最も一般的な方法は Python アプリケーションは、Thread クラスを継承し、その run() メソッドをオーバーライドするクラスを宣言することです。

要約すると、Thread クラスは、別個のスレッドで実行されるコード シーケンスを意味します。 制御の。

したがって、マルチスレッド アプリを作成するときは、次の操作を行います。

  1. Threadクラスを継承するクラスを定義する
  2. 上書きする __init__ コンストラクタ
  3. 上書きする run() 方法

スレッド オブジェクトが作成されると、 開始() このメソッドは、このアクティビティの実行を開始するために使用できます。 join() メソッドを使用すると、現在のアクティビティが終了するまで他のすべてのコードをブロックできます。

それでは、スレッドモジュールを使用して、先ほどの例を実装してみましょう。もう一度、 IDLE 次のように入力します。

import time
import threading

class threadtester (threading.Thread):
    def __init__(self, id, name, i):
       threading.Thread.__init__(self)
       self.id = id
       self.name = name
       self.i = i
       
    def run(self):
       thread_test(self.name, self.i, 5)
       print ("%s has finished execution " %self.name)

def thread_test(name, wait, i):

    while i:
       time.sleep(wait)
       print ("Running %s \n" %name)
       i = i - 1

if __name__=="__main__":
    thread1 = threadtester(1, "First Thread", 1)
    thread2 = threadtester(2, "Second Thread", 2)
    thread3 = threadtester(3, "Third Thread", 3)

    thread1.start()
    thread2.start()
    thread3.start()

    thread1.join()
    thread2.join()
    thread3.join()

上記のコードを実行すると、次の出力が表示されます。

バックストーリー: スレッド クラス

コードの説明

バックストーリー: スレッド クラス

  1. この部分は前の例と同じです。ここでは、実行と遅延を処理するために使用されるtimeモジュールとthreadモジュールをインポートします。 Python スレッド。
  2. このビットでは、threadtester というクラスを作成しています。これは、 スレッド スレッドモジュールのクラス。これは、スレッドを作成する最も一般的な方法の 1 つです。 Pythonただし、コンストラクタと run() アプリ内のメソッド。 上記のコードサンプルからわかるように、 __init__ メソッド (コンストラクター) がオーバーライドされました。 同様に、 run() 方法。 これには、スレッド内で実行するコードが含まれています。 この例では、thread_test() 関数を呼び出しています。
  3. これはthread_test()メソッドで、次の値を受け取ります。 i 引数として渡され、各イテレーションで 1 ずつ減らし、i が 0 になるまで残りのコードをループします。各イテレーションでは、現在実行中のスレッドの名前を出力し、wait 秒 (これも引数として渡されます) だけスリープします。
  4. thread1 = threadtester(1, “First Thread”, 1) ここでは、スレッドを作成し、__init__ で宣言した XNUMX つのパラメーターを渡しています。 最初のパラメータはスレッドの ID、XNUMX 番目のパラメータはスレッドの名前、XNUMX 番目のパラメータはカウンターであり、while ループを実行する回数を決定します。
  5. thread2.start() start メソッドは、スレッドの実行を開始するために使用されます。内部的には、start() 関数はクラスの run() メソッドを呼び出します。
  6. thread3.join() join() メソッドは、他のコードの実行をブロックし、呼び出されたスレッドが終了するまで待機します。

ご存知のとおり、同一プロセス内のスレッドは、そのプロセスのメモリとデータにアクセスできます。そのため、複数のスレッドが同時にデータを変更したりアクセスしようとすると、エラーが発生する可能性があります。

次のセクションでは、スレッドが既存のアクセス トランザクションをチェックせずにデータやクリティカル セクションにアクセスした場合に発生する可能性のあるさまざまな種類の問題について説明します。

デッドロックと競合状態

デッドロックと競合状態について学ぶ前に、並行プログラミングに関連するいくつかの基本的な定義を理解しておくと役立ちます。

  • クリティカルセクション: これは共有変数にアクセスまたは変更するコード断片であり、アトミックトランザクションとして実行されなければなりません。
  • コンテキストスイッチ: これは、CPUがスレッドの状態を保存してからタスクを切り替える際に、後で同じ時点から再開できるようにするプロセスです。

デッドロック

デッドロック は、開発者が並行/マルチスレッドアプリケーションを作成する際に直面する最も恐れている問題です。 Pythonデッドロックを理解する最良の方法は、コンピュータサイエンスの古典的な例題である ダイニング Philoソファーズ問題。

食事哲学者の問題提起は次のとおりです。

図に示すように、5人の哲学者が円卓に座り、5枚のスパゲッティ(パスタの一種)の皿と5本のフォークが置かれている。

ダイニング Philoソファーズ問題

ダイニング Philoソファーズ問題

哲学者は、常に食事をしているか、考え事をしているかのどちらかでなければなりません。

さらに、哲学者はスパゲッティを食べる前に、隣り合った 2 本のフォーク (つまり、左と右のフォーク) を取らなければなりません。デッドロックの問題は、5 人の哲学者全員が同時に右のフォークを手に取ったときに発生します。

哲学者たちはそれぞれフォークを 1 本ずつ持っているため、他の哲学者がフォークを置くまで待つことになります。その結果、誰もスパゲッティを食べることができません。

同様に、並行システムでは、異なるスレッドまたはプロセス (哲学者) が同時に共有システム リソース (フォーク) を取得しようとするとデッドロックが発生します。その結果、他のプロセスが保持する別のリソースを待機しているため、どのプロセスも実行する機会がありません。

レース条件

競合状態とは、システムが2つ以上の操作を同時に実行する際に発生する、プログラムの望ましくない状態のことです。例えば、次の単純なforループを考えてみましょう。

i=0; # a global variable
for x in range(100):
    print(i)
    i+=1;

作成する場合 n このコードを同時に実行するスレッドの数が複数ある場合、プログラムの実行終了時に、スレッド間で共有される変数 i の値を特定することはできません。これは、実際のマルチスレッド環境では、スレッドが重複して実行される可能性があり、あるスレッドによって取得および変更された i の値が、他のスレッドがアクセスした際に変化する可能性があるためです。

これらは、マルチスレッドまたは分散環境で発生する可能性のある2つの主要な問題クラスです。 Python アプリケーション。次のセクションでは、スレッドを同期させることでこの問題を解決する方法を学びます。

Syncスレッドを輝かせる

競合状態、デッドロック、その他のスレッドベースの問題に対処するために、スレッドモジュールは ロック オブジェクト。スレッドが特定のリソースにアクセスしたい場合、そのリソースのロックを取得するという考え方です。スレッドが特定のリソースをロックすると、ロックが解除されるまで他のスレッドはそのリソースにアクセスできなくなります。その結果、リソースへの変更はアトミックになり、競合状態が回避されます。

ロックは、 _糸 モジュール。ロックは、任意の時点で次の2つの状態のいずれかになります。 ロック or ロック解除。 次の XNUMX つの方法がサポートされています。

  1. 取得する(): ロック状態が解除されている場合、acquire() メソッドを呼び出すと状態がロック状態に変更され、処理が終了します。ただし、状態がロックされている場合は、他のスレッドによって release() メソッドが呼び出されるまで、acquire() の呼び出しはブロックされます。
  2. リリース(): release() メソッドは、状態をロック解除に設定する、つまりロックを解放するために使用されます。 これは、必ずしもロックを取得したスレッドではなく、任意のスレッドから呼び出すことができます。

アプリでロックを使用する例を以下に示します。 IDLE 次のように入力します。

import threading
lock = threading.Lock()

def first_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the first funcion')
        lock.release()

def second_function():
    for i in range(5):
        lock.acquire()
        print ('lock acquired')
        print ('Executing the second funcion')
        lock.release()

if __name__=="__main__":
    thread_one = threading.Thread(target=first_function)
    thread_two = threading.Thread(target=second_function)

    thread_one.start()
    thread_two.start()

    thread_one.join()
    thread_two.join()

ここで、F5 キーを押します。 次のような出力が表示されるはずです。

Syncスレッドを美しくする

コードの説明

Syncスレッドを美しくする

  1. ここでは、単に呼び出して新しいロックを作成しています。 threading.Lock() ファクトリー機能。 内部的には、Lock() は、プラットフォームによって維持される最も効果的な具体的な Lock クラスのインスタンスを返します。
  2. 最初のステートメントでは、acquire() メソッドを呼び出してロックを取得します。 ロックが許可されたら、次のように印刷します。 「ロックを取得しました」 コンソールに。 スレッドで実行するすべてのコードの実行が終了したら、release() メソッドを呼び出してロックを解放します。

理論は正しいのですが、ロックが実際に機能したことをどうやって確認するのでしょうか?出力結果を見ると、各print文が一度に正確に1行ずつ出力していることがわかります。以前の例では、複数のスレッドが同時にprint()メソッドにアクセスしていたため、printの出力が不規則になっていました。ここでは、print関数はロックが取得された後にのみ呼び出されます。そのため、出力は1行ずつ、1行ずつ表示されます。

錠前以外にも、 Python また、以下に示すように、スレッド同期を処理するための他のメカニズムもサポートしています。

  1. Rロック
  2. Semaphores
  3. の賃貸条件
  4. イベント、および
  5. 障壁

グローバル インタープリター ロック (およびその対処方法)

詳細に入る前に PythonGILを理解するために、これから説明するセクションを理解する上で役立つ用語をいくつか定義しておきましょう。

  1. CPUバウンドコード:これは、CPUによって直接実行されるコードを指します。
  2. I/Oバウンドコード:これは、OSを介してファイルシステムにアクセスするあらゆるコードを指します。
  3. CPython: 参照です 実装 of Python C言語で書かれたインタプリタとして記述することができ、 Python (プログラミング言語)。

GILとは何か Python?

グローバルインタプリタロック(GIL) in Python GILは、プロセスを扱う際に使用されるプロセスロックまたはミューテックスです。これにより、一度に1つのスレッドが特定のリソースにアクセスできることが保証され、オブジェクトとバイトコードが同時に使用されることも防止されます。これは、シングルスレッドプログラムのパフォーマンス向上に役立ちます。 Python 非常にシンプルで、実装も簡単です。

ロックを使用すると、特定の時点で XNUMX つのスレッドだけが特定のリソースにアクセスできるようにすることができます。

の機能のXNUMXつ Python それは、各インタプリタ プロセスでグローバル ロックを使用するということです。つまり、すべてのプロセスが Python インタープリタ自体をリソースとして捉える。

例えば、あなたが書いたとしましょう Python CPU操作とI/O操作の両方を実行するために2つのスレッドを使用するプログラムです。このプログラムを実行すると、次のようになります。

  1. その Python インタープリタは新しいプロセスを作成し、スレッドを生成します。
  2. スレッド 1 が実行を開始すると、まず GIL を取得してロックします。
  3. スレッド 2 がすぐに実行したい場合は、別のプロセッサが空いている場合でも、GIL が解放されるまで待つ必要があります。
  4. ここで、スレッド 1 が I/O 操作を待機しているとします。この時点で、スレッド 2 は GIL を解放し、スレッド XNUMX がそれを取得します。
  5. I/O 操作の完了後、スレッド 1 がすぐに実行したい場合は、スレッド 2 によって GIL が解放されるまで再度待機する必要があります。

このため、一度にインタープリタにアクセスできるスレッドは1つだけとなり、実行されるスレッドは1つだけになります。 Python ある時点におけるコード。

シングルコアプロセッサの場合は、タイムスライシング(このチュートリアルの最初のセクションを参照)を使用してスレッドを処理するため、問題ありません。しかし、マルチコアプロセッサの場合、複数のスレッドで実行されるCPUバウンド関数は、利用可能なすべてのコアを同時に使用するわけではないため、プログラムの効率に大きな影響を与えます。

なぜ GIL が必要だったのでしょうか?

C言語Python ガベージコレクタは、参照カウントと呼ばれる効率的なメモリ管理技術を使用します。その仕組みは次のとおりです。 Python オブジェクトには参照カウントがあり、新しい変数名に代入されたり、コンテナ(タプル、リストなど)に追加されたりするたびに増加します。同様に、参照がスコープ外になったり、del文が呼び出されたりすると、参照カウントは減少します。オブジェクトの参照カウントが0になると、ガベージコレクションが行われ、割り当てられたメモリが解放されます。

しかし問題は、参照カウント変数が他のグローバル変数と同様に競合状態になりやすいことです。この問題を解決するために、 Python グローバルインタープリタロックを使用することにしました。もう一つの選択肢は、各オブジェクトにロックを追加することでしたが、その場合、デッドロックが発生し、acquire() と release() の呼び出しによるオーバーヘッドが増加する可能性がありました。

したがって、GILはマルチスレッドにとって重大な制約である。 Python CPU 負荷の高い処理を実行するプログラム (実質的にシングルスレッドになります)。アプリケーションで複数の CPU コアを使用する場合は、 マルチプロセッシング 代わりにモジュール。

よくあるご質問

ネットワーク呼び出し、ファイルアクセス、データベースクエリなど、I/O処理がボトルネックとなる処理にはスレッド処理を使用します。これらの処理では、スレッドは外部リソースの待機状態になります。一方、計算処理などCPU処理がボトルネックとなる処理にはマルチプロセッシングを使用します。マルチプロセッシングは複数のコアで実行され、GIL(グローバルインタプリタロック)をバイパスするためです。

デフォルトではそうではありません。PEP 703 では、オプションのフリースレッドビルドが導入されました。 Python 3.13はGILを無効にし、 Python 3.14でもその機能は継続されています。標準ビルドには引き続きGIL(グローバルインタプリタロック)が搭載されているため、ほとんどのプログラムは以前とまったく同じように動作します。

デーモンスレッドはバックグラウンドで実行され、プログラムの終了を妨げません。デーモンスレッドのみが残っている場合、 Python それらをシャットダウンして終了します。start() を呼び出す前に、thread.daemon = True を設定してください。

Thread クラスは run() の結果を直接返しません。値をスレッドオブジェクトまたは共有キューに格納するか、concurrent.futures.ThreadPoolExecutor を使用してください。concurrent.futures.ThreadPoolExecutor の submit() メソッドは Future を返し、result() メソッドで読み取ることができます。

ThreadPoolExecutor は、ワーカー スレッドのプールを管理する高レベルのヘルパーです。submit() または map() を使用してタスクを送信すると、スレッドの作成、再利用、およびクリーンアップを処理し、各結果を保持する Future オブジェクトを返します。

数百、数千ものスレッドを作成できますが、GIL により実行できるのは 1 つのスレッドのみです。 Python バイトコードはどの瞬間にも存在します。I/O 待機中は依然として重複するため、多数の同時実行スレッドは、CPU バウンドなプログラムよりも I/O バウンドなプログラムに遥かに効果的です。

はい、主にデータの読み込み、APIの呼び出し、ファイルの読み込みなどのI/Oバウンドなステップで使用されます。負荷の高いトレーニングはCPUバウンドなので、NumPyやPyなどのライブラリが使用されます。Torch は C コードで GIL を解放するか、真の並列処理のためにマルチプロセッシングに依存します。

はい。GitHub Copilotは、コメントや関数名から、スレッドのサブクラス、ロックの使用方法、ThreadPoolExecutorの設定など、よく使われるパターンを自動補完します。生成されたコードを使用する前に、必ず競合状態や適切なロック処理について確認してください。