内部からのPython。 プロセス構造

1. はじめに

2. オブジェクト。

3. オブジェクト。 しっぽ

4. プロセス構造



Pythonの内部に関する一連の記事の翻訳を続けています。 「どのように配置されているのか」と疑問に思ったことがあるなら、必ず読んでください。 著者は、言語の構造の多くの興味深い重要な側面に光を当てます。



前のパートでは、Pythonオブジェクトシステムについて説明しました。 トピックはまだ終わりではありませんが、先に進みましょう。



Pythonの実装について考えると、マシンコードが移動する巨大なコンベヤーを想像します。その後、巨大な工場に行き、そこでタワーとタワークレーンが至る所に立ち上がります。 このパートでは、インタプリタの状態構造 ストリーム状態./Python/pystate.c



)について説明します。 次に、後でバイトコードがどのように実行されるかを理解しやすくするために、基礎を築く必要があります。 すぐに、フレーム、名前空間、およびコードオブジェクトがどのように配置されるかがわかります。 しかし、最初に、すべてを結び付けるデータ構造について話しましょう。 少なくとも、 オペレーティングシステムの構造の表面的な理解と、少なくともカーネルプロセススレッドなどの用語に関する知識があることを念頭に置いてください。



多くのオペレーティングシステムでは、ユーザーコードはプロセスに存在するスレッドで実行されます(これは、ほとんどの* nixシステムおよび「モダン」バージョンのWindowsに当てはまります)。 カーネルは、プロセスとスレッドの準備と削除、およびどの論理CPUで実行されるスレッドの決定を担当します。 プロセスがPy_Initialize



関数を呼び出すと、別の抽象化であるインタープリターがシーンに入ります。 プロセスで実行されるPythonコードはすべてインタープリターにバインドされます。 インタプリタは、これから説明する他のすべての概念の基礎と考えることができます。 Pythonは、1つのプロセスで2つ(またはそれ以上)のインタープリターの初期化をサポートします。 この機能は実際にはめったに使用されないという事実にもかかわらず、私はそれを考慮します。 前述のように、コードはスレッドで実行されます。 例外なし、Python仮想マシン(VM)。 さらに、VM自体がスレッドをサポートしています。 Pythonには、スレッドを表すための独自の抽象化があります。 この抽象化の実装は、カーネルメカニズムに完全に依存しています。 したがって、カーネルとPythonの両方に、Pythonスレッドのそれぞれの概念があります。 これらのスレッドはカーネルによって管理され、システム内の他のすべてのスレッドと並行して個別のスレッドとして実行されます。 まあ...ほぼ並行して。



今まで、私たちは中国の店で象に注目していませんでした。 象の名前はGIL( Global Interpreter Lock )です。 何らかの理由で、CPythonの多くの側面はスレッドセーフではありません。 これには、利点(たとえば、多くのPythonオペレーターの実装の簡素化と原子性の保証)と欠点の両方があります。 主な欠点は、Pythonフローの並列実行を防止するメカニズムが必要なことです。そのようなメカニズムがないと、データが破損する可能性があるためです。 GILは、Pythonコードを実行する必要がある場合にスレッドが取得する必要があるプロセスレベルのロックです。 これにより、1つの論理CPUで同時に実行されるPythonスレッドの数が1つに制限されます。 Pythonスレッドは協調的なマルチタスクを実装し、自発的にGILを解放し、他のスレッドに作業の機会を与えます。 この機能は実行サイクルに組み込まれています。 通常のスクリプトやいくつかの拡張機能を記述する際に、このロックについて特に考慮する必要はありません(継続的に機能しているようです)。 スレッドはPython APIを使用しません(これは多くの場合に発生します)が、他のPythonスレッドと並行して動作する可能性があることに注意してください。 少し後でGILについて説明します。待ちきれない人 David Beasleyのプレゼンテーションを読むことができます。



プロセス(OS抽象化)、インタープリター(Python抽象化)、およびスレッド(OSおよびPython抽象化の両方)の概念を覚えています。 次に、次の方法を実行します。1つの操作で開始し、プロセス全体で終了します。



spam = eggs - 1



diss



)によって生成されたバイトコードをもう一度見てみましょう:



 >>> diss("spam = eggs - 1") 1 0 LOAD_NAME 0 (eggs) 3 LOAD_CONST 0 (1) 6 BINARY_SUBTRACT 7 STORE_NAME 1 (spam) 10 LOAD_CONST 1 (None) 13 RETURN_VALUE >>>
      
      





すべての作業を行う操作BINARY_SUBTRACT



加えて、操作BINARY_SUBTRACT



LOAD_NAME (eggs)



およびSTORE_NAME (spam)



ます。 明らかに、これらの操作を実行するには、場所が必要です。 eggs



はどこかから引き出され、 spam



はどこかから削除される必要がありspam



。 この場所は、コードが実行される内部データ構造( フレームオブジェクトコードオブジェクト)によって参照されます 。 Pythonコードを実行すると、フレームが実際に実行されます( ceval.c



PyEval_EvalFrameEx



思い出してPyEval_EvalFrameEx



)。 現在、フレームオブジェクトとコードオブジェクトの概念を混合しているため、今は簡単です。 これらの構造の違いについては、後ほど理解します。 ここで、フレームオブジェクトのf_back



フィールドに最も関心があります。 フレームn



このフィールドはフレームn-1



指します。 現在のフレームの原因となったフレーム(ストリームの最初のフレームはNULL



示しNULL



)。



フレームスタックは各スレッドに固有であり、スレッド固有の構造./Include.h/pystate.h



関連付けられています。これには、ストリームの現在の実行可能フレーム(最後に呼び出されたフレーム、スタックの最上部)へのポインターが含まれます。 PyThreadState



構造体PyThreadState



、作成されたスレッドがOS( ./Modules/_threadmodule.c



/ thread_PyThread_start_new_thread



thread_PyThread_start_new_thread



および>>> from _thread import start_new_thread



>>> from _thread import start_new_thread



要求される直前に、 _PyThreadState_Prealloc



関数によってプロセス内の各Pythonスレッドに割り当てられ、初期化されます。 プロセスでは、インタープリターによって制御されないスレッドも作成される場合があります。 PyThreadState



構造を持たないため、Python APIにアクセスしないでください。 これは主に組み込みアプリケーションで発生します。 ただし、そのようなスレッドは「pythonized」できるため、Pythonコードを実行できるようになり、新しいPyThreadState



構造を作成する必要があります。 1つのインタープリターが実行されている場合、このようなストリームの移行にAPIを使用できます。 通訳が複数いる場合は、手動で行う必要があります。 最後に、各フレームが前のフレームへのポインターを介して接続されるのとほぼ同じ方法で、スレッドの状態はポインターPyThreadState *next



リンクされたリストによって結合されます。



ストリーム構造のリストは、ストリームが配置されているインタープリターの構造に関連付けられています。 インタープリター構造は、. ./Include.h/pystate.h



/ ./Include.h/pystate.h



PyInterpreterState



定義されています。 プロセスでPython仮想マシンを初期化するPy_NewInterpreter



関数がPy_NewInterpreter



か、新しいインタープリター構造が作成さPy_NewInterpreter



関数がPy_NewInterpreter



と作成されます(プロセスに複数のインタープリターがある場合)。 よりよく理解するために、 Py_NewInterpreter



はインタープリター構造を返さず、新しいインタープリター用に新しく作成されたスレッドのPyThreadState



構造を返すことを思い出します。 単一のスレッドなしで新しいインタープリターを作成することは、フローのないプロセスでは意味をなさないように、あまり意味がありません。 プロセス内のインタープリターの構造は、インタープリター内のフローの構造と同じ方法で互いに関連しています。



一般に、単一の操作からプロセス全体への旅は完了しています。操作は実行可能なコードオブジェクト(「非実行」オブジェクトは通常のデータのように近くにあります)、コードオブジェクトはPythonの実行可能なフレーム-streams、およびストリームは、インタープリターに属します。 この構造のルートは、静的変数./Python/pystate.c



によって参照されます。 これは、最初のインタープリターの構造を示します(それを介して、他のすべてのインタープリター、ストリームなどが利用可能です)。 head_mutex



ミューテックスは、異なるスレッドからの変更を競合することにより、これらの構造の損傷を防ぎます(これはGILではなく、インタープリターとスレッド構造の通常のミューテックスであると指定します)。 このロックは、 HEAD_UNLOCK



HEAD_UNLOCK



によって制御されHEAD_UNLOCK



。 原則として、 interp_head



変数を追加するか、既存のインタープリターまたはストリームを削除する必要がある場合、 interp_head



変数interp_head



アクセスします。 プロセスに複数のインタープリターが存在する場合、現在実行中のストリームが配置されているインタープリターの構造は、この変数によって利用できるとは限りません。



変数./Python/pystate.c



を使用する方が安全_PyThreadState_Current



。これは、実行可能スレッドの構造を示します(いくつかの条件を考慮する必要があります)。 つまり、インタープリターに到達するためには、コードはそのストリームの構造を必要とし、そこからインタープリターを既に引き出すことができます。 この変数にアクセスするには(現在の値を取得するか、古い値を保持して変更します)、GILを保持する必要がある関数があります。 これは重要であり、これはCPythonのスレッドセーフの欠如により生じる問題の1つです。 変数_PyThreadState_Current



の値は、Pythonの初期化中または新しいスレッドの作成中に新しいスレッドの構造に設定されます。 Pythonスレッドは、起動後に最初に起動するとき、以下に依存します_PyThreadState_Current



)GILを保持し、b) _PyThreadState_Current



変数の値_PyThreadState_Current



正しい。 この時点で、スレッドはGILを提供すべきではありません。 最初に、値_PyThreadState_Current



をどこかに保存する必要があるため、次回GILをキャプチャするときに、変数の目的の値を復元して作業を続行できます。 この動作により、 _PyThreadState_Current



常に現在実行中のスレッドを指します。 この動作を実装するマクロPy_BEGIN_ALLOW_THREADS



Py_END_ALLOW_THREADS



があります。 GILとそれを何時間も使用するためAPIについて話すことができ、CPythonを他の実装(スレッドが同時に実行されるJythonやIronPythonなど)と比較するのは興味深いでしょう。 しかし、このトピックは今のところ延期しましょう。



図では、1つのプロセスの構造間の接続を示しています。このプロセスでは、2つのインタープリターがそれぞれ2つのスレッドで起動され、フレームのスタックを参照します。

画像



いいですね だから 彼らはすべてを議論しましたが、これらの構造の意味はまだ明らかではありません。 なぜ必要なのですか? それらの何が面白いですか? 複雑にしたくないので、いくつかの機能について簡単に説明します。 たとえば、インタープリターの構造には、インポートされたモジュールで動作するように設計されたフィールドがあります。 Unicodeを使用するために必要なポインター。 動的リンカーフラグフィールドと、プロファイリングにTSCを使用することに関連するフィールド(最後から2番目の段落を参照 )。



ストリーム構造の一部のフィールドは、このストリームの実行の詳細に関連付けられています。 たとえば、基礎となるプラットフォームスタックがオーバーフローしてプロセス全体がクラッシュする前に、深すぎる再帰をキャッチしてRuntimeError



例外をスローするには、 recursion_depth



overflow



およびrecursion_critical



が必要RuntimeError



。 また、例外のプロファイリング、トレース、および処理に関連するフィールドと、あらゆる種類のジャンクを格納するための辞書もあります。



Pythonプロセスの構造についての話はここで完了することができると思います。 すべてが明確であることを願っています。 次の投稿では、本物のハードコアに進み、 フレームオブジェクト名前空間、 コードオブジェクトについて説明します 。 準備をしなさい。



All Articles