グローバルインタープリターロック(つまりCPython)を備えたPythonの実装に依存していて、マルチスレッドコードを記述している場合、本当にロックが必要ですか?
GILで複数の命令を並行して実行することが許可されていない場合、保護するために共有データは不要ではないでしょうか。
これがばかげた質問である場合は申し訳ありませんが、マルチプロセッサ/コアマシン上のPythonについて私がいつも疑問に思っていたものです。
同じことが、GILを持つ他の言語実装にも当てはまります。
グローバルインタープリターロック(つまりCPython)を備えたPythonの実装に依存していて、マルチスレッドコードを記述している場合、本当にロックが必要ですか?
GILで複数の命令を並行して実行することが許可されていない場合、保護するために共有データは不要ではないでしょうか。
これがばかげた質問である場合は申し訳ありませんが、マルチプロセッサ/コアマシン上のPythonについて私がいつも疑問に思っていたものです。
同じことが、GILを持つ他の言語実装にも当てはまります。
スレッド間で状態を共有する場合は、引き続きロックが必要です。GIL はインタープリターを内部的に保護するだけです。独自のコードに一貫性のない更新を含めることができます。
例えば:
#!/usr/bin/env python
import threading
shared_balance = 0
class Deposit(threading.Thread):
def run(self):
for _ in xrange(1000000):
global shared_balance
balance = shared_balance
balance += 100
shared_balance = balance
class Withdraw(threading.Thread):
def run(self):
for _ in xrange(1000000):
global shared_balance
balance = shared_balance
balance -= 100
shared_balance = balance
threads = [Deposit(), Withdraw()]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print shared_balance
ここで、共有状態の読み取り ( balance = shared_balance
) と変更された結果の書き込み( ) の間でコードが中断されshared_balance = balance
、更新が失われる可能性があります。結果は、共有状態のランダムな値です。
更新の一貫性を保つために、 run メソッドは read-modify-write セクション (ループ内) の周りで共有状態をロックするか、共有状態が read になってからいつ変更されたかを検出する何らかの方法を持つ必要があります。
いいえ - GIL は、状態を変更する複数のスレッドから Python の内部を保護するだけです。これは非常に低レベルのロックであり、Python 自体の構造を一貫した状態に保つだけで十分です。独自のコードでスレッド セーフをカバーするために必要なアプリケーションレベルのロックについては説明しません。
ロックの本質は、コードの特定のブロックが 1 つのスレッドによってのみ実行されるようにすることです。GIL は 1 つのバイトコードのサイズのブロックに対してこれを強制しますが、通常はこれよりも大きなコード ブロックにロックを適用する必要があります。
議論への追加:
GIL が存在するため、一部の操作は Python ではアトミックであり、ロックを必要としません。
http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe
ただし、他の回答で述べたように、アプリケーション ロジックでロックが必要なときはいつでもロックを使用する必要があります (プロデューサー/コンシューマーの問題など)。
この投稿では、GIL についてかなり高いレベルで説明しています。
特に興味深いのは、次の引用です。
10 命令ごとに (このデフォルトは変更可能)、コアは現在のスレッドの GIL を解放します。その時点で、OS はロックを求めて競合するすべてのスレッドからスレッドを選択します (GIL を解放したばかりの同じスレッドを選択する可能性があります。どのスレッドが選択されるかを制御することはできません)。そのスレッドは GIL を取得し、さらに 10 個のバイトコードを実行します。
と
GIL は純粋な Python コードのみを制限することに注意してください。拡張機能 (通常は C で記述された外部の Python ライブラリ) は、ロックを解放するように記述できます。これにより、拡張機能がロックを再取得するまで、拡張機能とは別に Python インタープリターを実行できます。
GIL が提供するコンテキスト スイッチのインスタンスの数を減らし、各 Python インタープリター インスタンスに関して、マルチコア/プロセッサ システムをシングル コアとして動作させるように思えます。そのため、同期メカニズムを使用する必要があります。
グローバル インタープリター ロックは、スレッドが同時にインタープリターにアクセスするのを防ぎます (したがって、CPython は常に 1 つのコアしか使用しません)。ただし、私が理解しているように、スレッドはまだ中断され、プリエンプティブにスケジュールされています。つまり、スレッドがお互いのつま先を踏みつけないように、共有データ構造のロックがまだ必要です。
私が何度も遭遇した答えは、Python でのマルチスレッド化がオーバーヘッドに値することはめったにないということです。共有データ構造、キューなどを使用して、複数のプロセスをマルチスレッドと同じくらい「簡単」に実行できるようにするPyProcessingプロジェクトについて良いことを聞きました(PyProcessing は、マルチプロセッシングモジュールとして、次期 Python 2.6 の標準ライブラリに導入される予定です)。 .) これにより、各プロセスには独自のインタープリターがあるため、GIL を回避できます。
このように考えてください:
シングルプロセッサコンピュータでは、マルチスレッドは、1つのスレッドを一時停止し、同時に実行されているように見せるために十分な速度で別のスレッドを開始することによって発生します。これは、GILを使用したPythonのようなものです。実際に実行されているスレッドは1つだけです。
問題は、スレッドがどこでも中断される可能性があることです。たとえば、b =(a + b)* 3を計算する場合、次のような命令が生成される可能性があります。
1 a += b
2 a *= 3
3 b = a
ここで、それがスレッドで実行されており、そのスレッドが1行目または2行目で中断され、別のスレッドが開始されて実行されたとします。
b = 5
次に、他のスレッドが再開すると、bは古い計算値で上書きされますが、これはおそらく予期されていたものではありません。
したがって、実際には同時に実行されていなくても、ロックする必要があることがわかります。
それでもロックを使用する必要があります (コードは別のスレッドを実行するためにいつでも中断される可能性があり、これによりデータの不整合が生じる可能性があります)。GIL の問題は、Python コードが同時により多くのコア (または利用可能な場合は複数のプロセッサ) を使用できないことです。
ロックはまだ必要です。なぜそれらが必要なのかを説明してみます。
操作/命令はインタプリタで実行されます。GIL は、インタプリタが特定の時点で単一のスレッドによって保持されることを保証します。複数のスレッドを持つプログラムは、単一のインタープリターで動作します。特定の時点で、このインタープリターは単一のスレッドによって保持されます。これは、インタープリターを保持しているスレッドのみがいつでも実行されていることを意味します。
t1 と t2 などの 2 つのスレッドがあり、どちらもグローバル変数の値を読み取り、それをインクリメントする 2 つの命令を実行したいとします。
#increment value
global var
read_var = var
var = read_var + 1
上記のように、GIL は 2 つのスレッドが同時に命令を実行できないことのみを保証します。つまり、両方のスレッドがread_var = var
特定の瞬間に実行できないことを意味します。しかし、彼らは次々と命令を実行することができ、あなたはまだ問題を抱えている可能性があります. 次の状況を考慮してください。
read_var = var
ます。したがって、t1 の read_var は 0 です。GIL は、この読み取り操作がこの時点で他のスレッドに対して実行されないことのみを保証します。read_var = var
ます。しかし、read_var はまだ 0 です。したがって、t2 の read_var は 0 です。var = read_var+1
され、var が 1 になります。var = read_var+1
され、var が 1 になります。var
が 2 になることでした。Will Harris の例から少し更新します。
class Withdraw(threading.Thread):
def run(self):
for _ in xrange(1000000):
global shared_balance
if shared_balance >= 100:
balance = shared_balance
balance -= 100
shared_balance = balance
値チェックステートメントを引き出しに入れると、ネガティブなものはもう見られず、更新は一貫しているようです. 私の質問は:
GIL が、任意のアトミック タイムに 1 つのスレッドしか実行できない場合、古い値はどこにあるのでしょうか? 古い値がない場合、なぜロックが必要なのですか? (純粋な python コードについてのみ話すと仮定します)
私の理解が正しければ、上記の条件チェックは実際のスレッド環境では機能しません。複数のスレッドが同時に実行されている場合、古い値が作成される可能性があるため、共有状態の不一致が発生する可能性があるため、本当にロックが必要です。しかし、Python が実際に一度に 1 つのスレッドしか許可しない場合 (タイム スライス スレッド)、古い値が存在する可能性はありませんよね?