web-dev-qa-db-ja.com

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

グローバルインタープリターロック(つまりCPython)を持つPythonの実装に依存していて、マルチスレッドコードを記述している場合、本当にロックが必要ですか?

GILで複数の命令を並行して実行することが許可されていない場合、保護するために共有データは不要ではないでしょうか。

これがばかげた質問である場合は申し訳ありませんが、マルチプロセッサ/コアマシンのPythonについていつも疑問に思っていました。

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

68
Corey Goldberg

スレッド間で状態を共有する場合は、引き続きロックが必要です。 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セクション(ループ内)の周りの共有状態をロックするか、 共有状態が読み取られてからいつ変更されたかを検出する何らかの方法 を持つ必要があります。 =。

69
Will Harris

いいえ-GILはpython内部を複数のスレッドが状態を変更することから保護します。これは非常に低レベルのロックであり、Python自体の構造を一貫した状態に保つのに十分です。そうではありません。 applicationレベルロックをカバーして、独自のコードでスレッドセーフをカバーするために行う必要があります。

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

23
Brian

ディスカッションへの追加:

GILが存在するため、一部の操作はPythonでアトミックであり、ロックは必要ありません。

http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe

ただし、他の回答で述べられているように、stillは、アプリケーションロジックがロックを必要とするときはいつでも(プロデューサー/コンシューマーの問題などで)ロックを使用する必要があります。

10
Bruno Gomes

この投稿では、GILについてかなり高いレベルで説明しています。

特に興味深いのは、これらの引用です。

10命令ごとに(このデフォルトは変更可能)、コアは現在のスレッドのGILを解放します。その時点で、OSはロックを競合するすべてのスレッドからスレッドを選択します(おそらく、GILを解放したばかりの同じスレッドを選択します。どのスレッドを選択するかを制御することはできません)。そのスレッドはGILを取得してから、さらに10バイトコードを実行します。

そして

GILは純粋なPythonコードのみを制限することに注意してください。ロックを解放する拡張機能(外部Pythonライブラリは通常Cで記述されます))を記述できます。 Pythonインタープリターは、拡張機能がロックを再取得するまで、拡張機能とは別に実行されます。

GILは、コンテキストスイッチに可能なインスタンスを少なくし、マルチコア/プロセッサシステムを各pythonインタープリターインスタンスに関して、シングルコアとして動作させるようにします。そうです。まだ同期メカニズムを使用する必要があります。

9
rcreswick

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

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

8
David Eyk

このように考えてください:

シングルプロセッサコンピュータでは、マルチスレッドは、1つのスレッドを一時停止し、同時に実行されているように見せるために十分な速度で別のスレッドを開始することによって発生します。これは、GILを使用したPythonのようなものです。実際に実行されているスレッドは1つだけです。

問題は、スレッドをどこでも中断できることです。たとえば、b =(a + b)* 3を計算したい場合、次のような命令が生成される可能性があります。

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

ここで、それがスレッドで実行されており、そのスレッドが1行目または2行目で中断され、別のスレッドが開始されて実行されたとします。

b = 5

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

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

4
Dan

ロックはまだ必要です。なぜ必要なのか説明してみます。

すべての操作/命令はインタプリタで実行されます。 GILは、インタプリタが特定の瞬間で単一のスレッドによって保持されることを保証します。また、複数のスレッドを持つプログラムは、単一のインタープリターで機能します。特定の時点で、このインタープリターは単一のスレッドによって保持されます。これは、インタプリタを保持しているスレッドのみが常に実行中であることを意味します。

2つのスレッド(たとえばt1とt2)があり、両方がグローバル変数の値を読み取ってインクリメントする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は、この読み取り操作がこの瞬間に他のスレッドに対して実行されないことを保証するだけです。
  • GILはスレッドt2に与えられます。
  • t2はread_var = varを実行します。ただし、read_varはまだ0です。したがって、t2のread_varは0です。
  • GILはt1に与えられます。
  • t1はvar = read_var+1を実行し、varは1になります。
  • GILはt2に与えられます。
  • t2はread_var = 0と見なします。これは、それが読み取ったものだからです。
  • t2はvar = read_var+1を実行し、varは1になります。
  • 私たちの期待は、varが2になることでした。
  • したがって、アトミック操作として読み取りとインクリメントの両方を維持するには、ロックを使用する必要があります。
  • ウィル・ハリスの答えは、コード例を通してそれを説明します。
1
Akshar Raaj

それでもロックを使用する必要があります(別のスレッドを実行するためにコードがいつでも中断される可能性があり、これによりデータの不整合が発生する可能性があります)。 GILの問題は、Pythonコードが同時により多くのコア(または使用可能な場合は複数のプロセッサ)を使用できないことです。

1
rslite

ウィルハリスの例からの少しの更新:

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コード)についてのみ話していると仮定します)

私が正しく理解していれば、上記の条件チェックはrealスレッド環境では機能しません。複数のスレッドが同時に実行されている場合、古い値が作成される可能性があるため、共有状態の不整合が発生する可能性があり、実際にはロックが必要です。しかし、pythonが実際には一度に1つのスレッドしか許可しない場合(タイムスライススレッド)、古い値が存在する可能性はありませんよね?

0
jimx