例を使用した Python でのマルチスレッド化:Python で GIL を学ぶ
Python プログラミング言語を使用すると、マルチプロセッシングまたはマルチスレッドを使用できます。このチュートリアルでは、Python でマルチスレッド アプリケーションを作成する方法を学習します。
スレッドとは
スレッドは、並行プログラミングの実行単位です。マルチスレッドは、CPU が 1 つのプロセスの多くのタスクを同時に実行できるようにする技術です。これらのスレッドは、プロセス リソースを共有しながら個別に実行できます。
プロセスとは?
プロセスは基本的に実行中のプログラムです。コンピュータでアプリケーション (ブラウザやテキスト エディタなど) を起動すると、オペレーティング システムによって プロセスが作成されます。
Python のマルチスレッドとは?
Python でのマルチスレッド プログラミングは、プロセス内の複数のスレッドがデータ空間をメインスレッドと共有するよく知られた手法であり、スレッド内での情報の共有と通信を簡単かつ効率的にします。スレッドはプロセスよりも軽量です。マルチスレッドは、プロセス リソースを共有しながら個別に実行できます。マルチスレッドの目的は、複数のタスクと機能セルを同時に実行することです。
このチュートリアルでは、
- スレッドとは?
- プロセスとは?
- マルチスレッドとは?
- マルチプロセッシングとは?
- Python マルチスレッドとマルチプロセッシング
- マルチスレッドを使用する理由
- Python マルチスレッディング
- Thread および Threading モジュール
- スレッド モジュール
- スレッド化モジュール
- デッドロックと競合状態
- スレッドの同期
- GIL とは?
- なぜ GIL が必要だったのですか?
マルチプロセッシングとは
マルチプロセッシングを使用すると、関連のない複数のプロセスを同時に実行できます。これらのプロセスはリソースを共有せず、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 モジュールと比較すると、幅広い機能を提供します。

以下は、このモジュールで定義されているいくつかの便利な関数のリストです:
関数名 | 説明 |
---|---|
activeCount() | スレッドの数を返します まだ生きているオブジェクト |
currentThread() | Thread クラスの現在のオブジェクトを返します。 |
enumerate() | すべてのアクティブな Thread オブジェクトを一覧表示します。 |
isDaemon() | スレッドがデーモンの場合は true を返します。 |
isAlive() | スレッドがまだ生きている場合は true を返します。 |
スレッド クラス メソッド | |
start() | スレッドのアクティビティを開始します。複数回呼び出すと実行時エラーがスローされるため、スレッドごとに 1 回だけ呼び出す必要があります。 |
run() | このメソッドはスレッドのアクティビティを示し、Thread クラスを拡張するクラスによってオーバーライドできます。 |
join() | join() メソッドが呼び出されたスレッドが終了するまで、他のコードの実行をブロックします。 |
バックストーリー:スレッド クラス
threading モジュールを使用してマルチスレッド プログラムのコーディングを開始する前に、Thread クラスについて理解することが重要です。スレッド クラスは、Python でテンプレートとスレッドの操作を定義する主要なクラスです。
マルチスレッドの Python アプリケーションを作成する最も一般的な方法は、Thread クラスを拡張し、その run() メソッドをオーバーライドするクラスを宣言することです。
Thread クラスは、要約すると、別のスレッドで実行されるコード シーケンスを意味します。
したがって、マルチスレッド アプリを作成するときは、次のことを行います。
<オール>スレッド オブジェクトが作成されると、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()
これは、上記のコードを実行したときの出力です:
コードの説明
ご存知のように、同じプロセスにあるスレッドは、そのプロセスのメモリとデータにアクセスできます。その結果、複数のスレッドがデータを同時に変更またはアクセスしようとすると、エラーが忍び寄る可能性があります。
次のセクションでは、スレッドが既存のアクセス トランザクションをチェックせずにデータとクリティカル セクションにアクセスするときに発生する可能性があるさまざまな種類の問題について説明します。
デッドロックと競合状態
デッドロックと競合状態について学習する前に、並行プログラミングに関連するいくつかの基本的な定義を理解しておくと役に立ちます。
- クリティカル セクション共有変数にアクセスまたは変更するコードの断片であり、アトミック トランザクションとして実行する必要があります。
- コンテキスト スイッチは、あるタスクから別のタスクに変更する前にスレッドの状態を保存して、後で同じ時点から再開できるようにするために 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 つの方法をサポートしています:
<オール>アプリでロックを使用する例を次に示します。 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 を押してください。次のような出力が表示されるはずです:
コードの説明
理論には問題ありませんが、ロックが実際に機能したことをどのように確認できますか?出力を見ると、各 print ステートメントが一度に正確に 1 行ずつ印刷されていることがわかります。前の例で、複数のスレッドが同時に print() メソッドにアクセスしていたため、print からの出力が無計画だったことを思い出してください。ここで、印刷関数は、ロックが取得された後にのみ呼び出されます。そのため、出力は一度に 1 行ずつ表示されます。
ロックとは別に、Python は、以下にリストされているように、スレッド同期を処理するための他のメカニズムもサポートしています:
<オール>グローバル インタープリター ロック (およびその対処方法)
Python の GIL の詳細に入る前に、次のセクションを理解するのに役立ついくつかの用語を定義しましょう:
<オール>Python の GIL とは?
グローバル通訳者ロック (GIL) Python では、プロセスの処理中に使用されるプロセス ロックまたはミューテックスです。一度に 1 つのスレッドが特定のリソースにアクセスできるようにし、オブジェクトとバイトコードが同時に使用されるのを防ぎます。これにより、シングルスレッド プログラムのパフォーマンスが向上します。 Python の GIL は非常にシンプルで実装が簡単です。
ロックを使用して、特定のリソースに一度に 1 つのスレッドだけがアクセスできるようにすることができます。
Python の機能の 1 つは、各インタープリター プロセスでグローバル ロックを使用することです。これは、すべてのプロセスが Python インタープリター自体をリソースとして扱うことを意味します。
たとえば、2 つのスレッドを使用して CPU と「I/O」操作の両方を実行する Python プログラムを作成したとします。このプログラムを実行すると、次のようになります:
<オール>このため、いつでも 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