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

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

スレッドとは

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

プロセスとは

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

マルチスレッドとは Python?

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

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

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

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

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

すべてのプロセスには 2 つの基本コンポーネントがあります。

  • コード
  • データ

現在、プロセスには、と呼ばれる 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. これらのステートメントは、実行と遅延を処理するために使用される時間とスレッドモジュールをインポートします。 Python スレッド。
  2. ここでは、という関数を定義しました。 スレッドテスト、 によって呼び出されます 新しいスレッドの開始 方法。 この関数は while ループを XNUMX 回繰り返し実行し、それを呼び出したスレッドの名前を出力します。 反復が完了すると、スレッドが実行を終了したことを示すメッセージが出力されます。
  3. これはプログラムのメインセクションです。 ここでは、単に 新しいスレッドの開始 メソッドと スレッドテスト これにより、引数として渡した関数の新しいスレッドが作成され、実行が開始されます。 これを置き換えることができることに注意してください(スレッド_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. この部分は前の例と同じです。ここでは、実行と遅延を処理するために使用される時間とスレッドのモジュールをインポートします。 Python スレッド。
  2. このビットでは、threadtester というクラスを作成しています。これは、 スレッド スレッドモジュールのクラス。 これは、Python でスレッドを作成する最も一般的な方法の XNUMX つです。 ただし、オーバーライドするのはコンストラクターと run() アプリ内のメソッド。 上記のコードサンプルからわかるように、 __init__ メソッド (コンストラクター) がオーバーライドされました。 同様に、 run() 方法。 これには、スレッド内で実行するコードが含まれています。 この例では、thread_test() 関数を呼び出しています。
  3. これは、次の値を受け取る thread_test() メソッドです。 i 引数として、反復ごとに 1 ずつ減らし、i が 0 になるまでコードの残りの部分をループします。各反復で、現在実行中のスレッドの名前を出力し、待機秒間スリープします (これも引数として取られます) )。
  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 の値が、他のスレッドがそれにアクセスする間に変更される可能性があるためです。

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

Syncスレッドを輝かせる

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

ロックは、 __スレッド モジュール。 ロックは常に、次の 2 つの状態のいずれかになります。 ロック or ロック解除。 次の XNUMX つの方法がサポートされています。

  1. 取得()ロック状態がロック解除されている場合、acquire() メソッドを呼び出すと、状態がロック状態に変更されて戻ります。 ただし、状態がロックされている場合、他のスレッドによって release() メソッドが呼び出されるまで、acquire() の呼び出しはブロックされます。
  2. release()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 行ずつ表示されます。

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

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

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

Python の GIL の詳細に入る前に、次のセクションを理解するのに役立ついくつかの用語を定義しましょう。

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

GILとは何か Python?

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

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

の機能のXNUMXつ Python 各インタープリタ プロセスでグローバル ロックを使用する点が異なります。つまり、すべてのプロセスが Python インタープリタ自体をリソースとして扱います。

たとえば、CPU と 'I/O' 操作の両方を実行するために 2 つのスレッドを使用する Python プログラムを作成したとします。このプログラムを実行すると、次のことが起こります。

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

このため、インタプリタにアクセスできるスレッドは常に XNUMX つだけです。つまり、特定の時点で Python コードを実行するスレッドは XNUMX つだけになります。

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

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

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

しかし、問題は、参照カウント変数が他のグローバル変数と同様に競合状態になりやすいことです。この問題を解決するために、Python の開発者はグローバル インタープリタ ロックを使用することを決定しました。他のオプションは、各オブジェクトにロックを追加することでしたが、デッドロックが発生し、acquire() および release() 呼び出しのオーバーヘッドが増加しました。

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

製品概要

  • Python マルチスレッド用の 2 つのモジュールをサポートします。
    1. __スレッド module: スレッド化のための低レベル実装を提供しますが、廃止されました。
    2. スレッドモジュール: マルチスレッドの高レベル実装を提供し、現在の標準です。
  • スレッド モジュールを使用してスレッドを作成するには、次の手順を実行する必要があります。
    1. を拡張するクラスを作成します。 スレッド とに提供されます。
    2. そのコンストラクター (__init__) をオーバーライドします。
    3. その上書き run() 方法。
    4. このクラスのオブジェクトを作成します。
  • スレッドは、 開始() 方法。
  • その join() このメソッドを使用すると、このスレッド (結合が呼び出されたスレッド) が実行を終了するまで他のスレッドをブロックできます。
  • 競合状態は、複数のスレッドが共有リソースに同時にアクセスまたは変更すると発生します。
  • それは次の方法で回避できます Sync栄光のスレッド。
  • Python スレッドを同期する 6 つの方法をサポートします。
    1. ロック
    2. Rロック
    3. Semaphores
    4. の賃貸条件
    5. イベント、および
    6. 障壁
  • ロックを使用すると、ロックを取得した特定のスレッドのみがクリティカル セクションに入ることができます。
  • ロックには 2 つの主なメソッドがあります。
    1. 取得(): ロック状態を に設定します。 ロックされています。 ロックされたオブジェクトに対して呼び出された場合、リソースが解放されるまでブロックされます。
    2. release(): ロック状態を に設定します。 ロック解除 そして戻ってきます。 ロック解除されたオブジェクトに対して呼び出された場合は、 false を返します。
  • グローバルインタプリタロックは、1つのCだけがPython 一度に実行できるインタープリタ プロセス。
  • これはCの参照カウント機能を容易にするために使用されました。Pythonsのガベージコレクター。
  • にするには Python CPU に負荷のかかる操作が多いアプリの場合は、マルチプロセッシング モジュールを使用する必要があります。