GILのため、マルチスレッドのPythonコードではロックが不要ですか?


Answers

いいえ - GILはPythonの内部を複数のスレッドから保護して、状態を変更します。 これは非常に低レベルのロックで、Pythonの独自の構造を一貫した状態に保つのに十分なだけです。 独自のコードでスレッドの安全性をカバーするために必要なアプリケーションレベルのロックは対象としていません。

ロックの本質は、特定のコードブロックが1つのスレッドでのみ実行されるようにすることです。 GILは、単一のバイトコードのサイズのブロックに対してこれを強制しますが、通常は、ロックよりも大きなコードブロックにまたがるようにします。

Question

グローバルインタープリタロック(すなわちCPython)を備え、マルチスレッドコードを書くPythonの実装に頼っているのであれば、本当にロックが必要ですか?

GILが複数の命令を並行して実行できない場合、共有するデータを保護する必要はありませんか?

申し訳ありませんが、これは愚かな疑問ですが、マルチプロセッサ/コアマシン上で私がいつもPythonについて疑問に思っていたことです。

同じことが、GILを持つ他の言語実装にも当てはまります。




それをこのように考える:

単一プロセッサのコンピュータでは、マルチスレッドは、あるスレッドを一時停止し、別のスレッドを同時に起動して、同時に実行しているように見せかけることによって行われます。 これはPythonのGILと似ています。実際には1つのスレッドしか実行されていません。

問題は、例えばb =(a + b)* 3を計算したい場合など、スレッドをどこにでも中断させることができるということです。

1    a += b
2    a *= 3
3    b = a

さて、それはスレッドで実行されていて、そのスレッドは1行目または2行目の後に中断され、別のスレッドが起動して実行されているとします。

b = 5

次に、もう一方のスレッドが再開すると、bは古い計算値によって上書きされますが、これはおそらく予期されたものではありません。

したがって、実際には同時に実行されていないにもかかわらず、ロックが必要であることがわかります。




グローバルインタープリタロックは、スレッドがインタプリタに同時にアクセスするのを防ぎます(したがって、CPythonは1つのコアのみを使用します)。 しかし、私が理解しているように、スレッドはまだ中断されており、 先取り的にスケジューリングされています。つまり、スレッドが互いの足を踏まないように、共有データ構造にロックが必要です。

私が何度も遭遇した答えは、Pythonでのマルチスレッドはオーバーヘッドにはあまり価値がないということです。 私はPyProcessingプロジェクトに関して良い話を聞いたことがあります。これは、複数のプロセスを、共有データ構造やキューなどで、マルチスレッドとして「シンプル」に実行するようにします(PyProcessingは、今後のPython 2.6の標準ライブラリにマルチプロセッシングモジュール)これは、各プロセスが独自のインタープリタを持っているので、GILの周りにいます。




ロックはまだ必要です。 彼らがなぜ必要なのか説明しようと思います。

任意の操作/命令がインタプリタ内で実行される。 GILは、インタプリタが特定の瞬間に 1つのスレッドによって保持されることを保証します。 また、複数のスレッドを持つプログラムは1つのインタプリタで動作します。 特定の瞬間に、このインタプリタは1つのスレッドによって保持されます。 インタプリタを保持しているスレッドだけが、いつでも実行されていることを意味します。

たとえば、t1とt2の2つのスレッドがあり、両方ともグローバル変数の値を読み込んでインクリメントしている2つの命令を実行したいとします。

#increment value
global var
read_var = var
var = read_var + 1

上記のように、GILは2つのスレッドが同時に命令を実行できないことを保証します。つまり、どちらのスレッドも特定の瞬間にread_var = varを実行できません。 しかし、彼らは次々に命令を実行することができ、あなたはまだ問題を抱えている可能性があります。 この状況を考えてみましょう。

  • read_varが0であるとします。
  • GILはスレッドt1によって保持されます。
  • t1は、 read_var = var実行します。 したがって、t1のread_varは0です。GILは、この瞬間に他のスレッドに対してこの読み取り操作が実行されないことを保証します。
  • スレッドt2にGILが与えられる。
  • t2は、 read_var = var実行します。 しかし、read_varはまだ0です。したがって、t2のread_varは0です。
  • t1にGILを与える。
  • t1はvar = read_var+1を実行し、varは1になります。
  • t2にGILを与える。
  • t2はread_var = 0とみなします。なぜなら、それはそれが何を読むかだからです。
  • t2はvar = read_var+1を実行し、varは1になります。
  • 私たちの予想は、 varは2になるはずです。
  • したがって、読み取りとインクリメントの両方をアトミック操作として保持するには、ロックを使用する必要があります。
  • Harrisの答えは、コード例を使って説明します。