工業製造
産業用モノのインターネット | 工業材料 | 機器のメンテナンスと修理 | 産業プログラミング |
home  MfgRobots >> 工業製造 >  >> Industrial programming >> Python

例を使用した Python でのマルチスレッド化:Python で GIL を学ぶ

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

スレッドとは

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

プロセスとは?

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

Python のマルチスレッドとは?

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

このチュートリアルでは、

マルチプロセッシングとは

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

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

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

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

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

つまり、OS が独立して実行できる一連の命令です。 1 つのプロセス内のスレッドは、そのプロセスのデータを共有し、並列処理を促進するために連携するように設計されています。

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

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

Python マルチスレッド

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

<オール>
  • スレッド モジュール、および
  • スレッド化 モジュール
  • ただし、Python には、グローバル インタープリター ロック (GIL) と呼ばれるものもあります。パフォーマンスが大幅に向上するわけではなく、低下することさえあります 一部のマルチスレッド アプリケーションのパフォーマンス。このチュートリアルの以降のセクションですべてを学習します。

    Thread および Threading モジュール

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

    ただし、thread モジュールは長い間廃止されてきました。 Python 3 以降、廃止され、__thread としてのみアクセス可能になりました。 後方互換性のため。

    より高いレベルのスレッド化を使用する必要があります 展開する予定のアプリケーションのモジュール。スレッド モジュールは、ここでは教育目的でのみ取り上げています。

    スレッド モジュール

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

    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 キーを押してプログラムを実行します。すべてが正しく行われた場合、次の出力が表示されます:

    競合状態とその処理方法については、以降のセクションで詳しく説明します

    コードの説明

    <オール>
  • これらのステートメントは、Python スレッドの実行と遅延を処理するために使用される時間とスレッド モジュールをインポートします。
  • ここで、thread_test という関数を定義しました。 start_new_thread によって呼び出されます 方法。関数は while ループを 4 回実行し、それを呼び出したスレッドの名前を出力します。反復が完了すると、スレッドの実行が終了したことを示すメッセージが出力されます。
  • これはプログラムのメイン セクションです。ここでは、 start_new_thread を呼び出すだけです thread_test を使用したメソッド これにより、引数として渡した関数の新しいスレッドが作成され、実行が開始されます。これを置き換えることができることに注意してください (thread_ test) をスレッドとして実行する他の関数と組み合わせて使用​​します。
  • スレッド化モジュール

    このモジュールは、Python でのスレッド化の高レベルの実装であり、マルチスレッド アプリケーションを管理するためのデファクト スタンダードです。 thread モジュールと比較すると、幅広い機能を提供します。

    以下は、このモジュールで定義されているいくつかの便利な関数のリストです:

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

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

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

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

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

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

    <オール>
  • Thread クラスを拡張するクラスを定義する
  • __init__ をオーバーライドする コンストラクタ
  • run() をオーバーライドする メソッド
  • スレッド オブジェクトが作成されると、start() メソッドを使用して、このアクティビティの実行を開始し、join() メソッドを使用して、現在のアクティビティが終了するまで他のすべてのコードをブロックできます。

    では、threading モジュールを使用して前の例を実装してみましょう。再び、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()
    

    これは、上記のコードを実行したときの出力です:

    コードの説明

    <オール>
  • この部分は、前の例と同じです。ここでは、Python スレッドの実行と遅延を処理するために使用される時間とスレッド モジュールをインポートします。
  • この部分では、Thread を継承または拡張する threadtester というクラスを作成します。 threading モジュールのクラス。これは、Python でスレッドを作成する最も一般的な方法の 1 つです。ただし、コンストラクタと run() のみをオーバーライドする必要があります アプリのメソッド。上記のコード サンプルでわかるように、__init__ メソッド (コンストラクター) がオーバーライドされました。同様に、 run() もオーバーライドしました 方法。スレッド内で実行するコードが含まれています。この例では、thread_test() 関数を呼び出しています。
  • これは i の値を取る thread_test() メソッドです。 引数として、各反復でそれを 1 減らし、i が 0 になるまで残りのコードをループします。各反復で、現在実行中のスレッドの名前を出力し、待機秒の間スリープします (これも引数として使用されます)。 ).
  • thread1 =threadtester(1, “First Thread”, 1) ここでは、スレッドを作成し、__init__ で宣言した 3 つのパラメーターを渡します。最初のパラメータはスレッドの ID、2 番目のパラメータはスレッドの名前、3 番目のパラメータはカウンタで、while ループを実行する回数を決定します。
  • thread2.start()start メソッドは、スレッドの実行を開始するために使用されます。内部的に、start() 関数はクラスの run() メソッドを呼び出します。
  • thread3.join() join() メソッドは他のコードの実行をブロックし、呼び出されたスレッドが終了するまで待機します。
  • ご存知のように、同じプロセスにあるスレッドは、そのプロセスのメモリとデータにアクセスできます。その結果、複数のスレッドがデータを同時に変更またはアクセスしようとすると、エラーが忍び寄る可能性があります。

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

    デッドロックと競合状態

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

    • クリティカル セクション共有変数にアクセスまたは変更するコードの断片であり、アトミック トランザクションとして実行する必要があります。
    • コンテキスト スイッチは、あるタスクから別のタスクに変更する前にスレッドの状態を保存して、後で同じ時点から再開できるようにするために CPU が従うプロセスです。

    デッドロック

    デッドロックは、Python で並行/マルチスレッド アプリケーションを作成する際に開発者が直面する最も恐れられる問題です。デッドロックを理解する最善の方法は、食事の哲学者問題として知られる古典的なコンピュータ サイエンスの例の問題を使用することです。

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

    図に示すように、5 人の哲学者が 5 皿のスパゲッティ (パスタの一種) と 5 つのフォークを備えた円卓に座っています。

    哲学者はいつでも、食べているか考えている必要があります。

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

    各哲学者は 1 つのフォークを持っているため、他の哲学者がフォークを下ろすのを待ちます。その結果、誰もスパゲッティを食べることができなくなります.

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

    レー​​ス条件

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

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

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

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

    スレッドの同期

    競合状態、デッドロック、およびその他のスレッドベースの問題に対処するために、スレッド モジュールは ロック 物体。アイデアは、スレッドが特定のリソースにアクセスする必要がある場合、そのリソースのロックを取得するというものです。スレッドが特定のリソースをロックすると、ロックが解放されるまで、他のスレッドはそのリソースにアクセスできなくなります。その結果、リソースへの変更はアトミックになり、競合状態が回避されます。

    ロックは、 __thread によって実装される低レベルの同期プリミティブです。 モジュール。いつでも、ロックは次の 2 つの状態のいずれかになります:ロック済み またはロック解除。 2 つの方法をサポートしています:

    <オール>
  • acquire() ロック状態がロック解除されている場合、acquire() メソッドを呼び出すと、状態がロック状態に変更されて戻ります。ただし、状態がロックされている場合、acquire() の呼び出しは、他のスレッドによって 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 を押してください。次のような出力が表示されるはずです:

    コードの説明

    <オール>
  • ここでは、threading.Lock() を呼び出して新しいロックを作成しているだけです。 工場機能。内部的には、Lock() は、プラットフォームによって維持される最も効果的な具象 Lock クラスのインスタンスを返します。
  • 最初のステートメントでは、acquire() メソッドを呼び出してロックを取得します。ロックが許可されたら、「ロックを取得しました」を出力します コンソールに。スレッドに実行させたいすべてのコードの実行が終了したら、release() メソッドを呼び出してロックを解放します。
  • 理論には問題ありませんが、ロックが実際に機能したことをどのように確認できますか?出力を見ると、各 print ステートメントが一度に正確に 1 行ずつ印刷されていることがわかります。前の例で、複数のスレッドが同時に print() メソッドにアクセスしていたため、print からの出力が無計画だったことを思い出してください。ここで、印刷関数は、ロックが取得された後にのみ呼び出されます。そのため、出力は一度に 1 行ずつ表示されます。

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

    <オール>
  • Rロック
  • セマフォ
  • 条件
  • イベント、および
  • 障壁
  • グローバル インタープリター ロック (およびその対処方法)

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

    <オール>
  • CPU バウンド コード:これは、CPU によって直接実行されるコードの一部を指します。
  • I/O バウンド コード:これは、OS を介してファイル システムにアクセスする任意のコードです
  • CPython:リファレンスの実装です C と Python (プログラミング言語) で書かれたインタープリターとして説明できます。
  • Python の GIL とは?

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

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

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

    たとえば、2 つのスレッドを使用して CPU と「I/O」操作の両方を実行する Python プログラムを作成したとします。このプログラムを実行すると、次のようになります:

    <オール>
  • Python インタープリターが新しいプロセスを作成し、スレッドを生成します
  • スレッド 1 が実行を開始すると、まず GIL を取得してロックします。
  • スレッド 2 が今すぐ実行したい場合は、別のプロセッサが空いていても、GIL が解放されるまで待たなければなりません。
  • ここで、スレッド 1 が I/O 操作を待っているとします。この時点で GIL を解放し、thread-2 がそれを取得します。
  • I/O ops の完了後、スレッド 1 がすぐに実行したい場合は、スレッド 2 によって GIL が解放されるまで再び待機する必要があります。
  • このため、いつでも 1 つのスレッドのみがインタープリターにアクセスできます。つまり、特定の時点で Python コードを実行するスレッドは 1 つだけになります。

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

    なぜ GIL が必要だったのですか?

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

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

    したがって、GIL は、大量の CPU バウンド操作を実行するマルチスレッド Python プログラムにとって重大な制限です (実質的にシングルスレッド化します)。アプリケーションで複数の CPU コアを利用したい場合は、マルチプロセッシング

    まとめ

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

    Python

    1. C ライブラリの free() 関数:使い方は?例で学ぶ
    2. 例を使用した Python 文字列 strip() 関数
    3. 例を使用した Python 文字列 count()
    4. 例を使用した Python round() 関数
    5. 例を使用した Python map() 関数
    6. Python Timeit() と例
    7. 例を使用したコレクション内の Python カウンター
    8. 例を使用した Python List count()
    9. Python List index() と例
    10. C# - マルチスレッド
    11. Python - マルチスレッド プログラミング