スレッドでグローバル変数を使用する


84

グローバル変数をスレッドと共有するにはどうすればよいですか?

私のPythonコード例は次のとおりです。

from threading import Thread
import time
a = 0  #global variable

def thread1(threadname):
    #read variable "a" modify by thread 2

def thread2(threadname):
    while 1:
        a += 1
        time.sleep(1)

thread1 = Thread( target=thread1, args=("Thread-1", ) )
thread2 = Thread( target=thread2, args=("Thread-2", ) )

thread1.join()
thread2.join()

2つのスレッドで1つの変数を共有する方法がわかりません。

回答:


97

その関数に対してローカルであるを変更しないようにa、でグローバルとして宣言する必要があります。thread2a

def thread2(threadname):
    global a
    while True:
        a += 1
        time.sleep(1)

ではthread1、の値を変更しようとしない限り、特別なことをする必要はありませんa(これにより、グローバル変数をシャドウするローカル変数が作成されglobal aます。必要に応じて使用してください)>

def thread1(threadname):
    #global a       # Optional if you treat a as read-only
    while a < 10:
        print a

47

関数内:

a += 1

コンパイラによって、として解釈されassign to a => Create local variable aますが、これは必要なものではありません。a not initialized(ローカル)aが実際に初期化されていないため、おそらくエラーで失敗します。

>>> a = 1
>>> def f():
...     a += 1
... 
>>> f()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in f
UnboundLocalError: local variable 'a' referenced before assignment

あなたは次のように(非常に眉をひそめている、そして正当な理由で)globalキーワードであなたが望むものを手に入れるかもしれません:

>>> def f():
...     global a
...     a += 1
... 
>>> a
1
>>> f()
>>> a
2

ただし、一般的には、すぐに手に負えなくなるグローバル変数の使用は避けてください。そして、これはマルチスレッドプログラムに特に当てはまります。マルチスレッドプログラムでは、いつ変更されたthread1かを知るための同期メカニズムがありませんa。つまり、スレッドは複雑であり、2つ(またはそれ以上)のスレッドが同じ値で動作するときにイベントが発生する順序を直感的に理解することは期待できません。言語、コンパイラ、OS、プロセッサ...はすべて役割を果たすことができ、速度、実用性、またはその他の理由で操作の順序を変更することを決定できます。

この種の適切な方法は、Python共有ツール(ロック とフレンド)を使用することです。または、次のように、データを共有するのではなく、キューを介してデータを通信することをお勧めします。

from threading import Thread
from queue import Queue
import time

def thread1(threadname, q):
    #read variable "a" modify by thread 2
    while True:
        a = q.get()
        if a is None: return # Poison pill
        print a

def thread2(threadname, q):
    a = 0
    for _ in xrange(10):
        a += 1
        q.put(a)
        time.sleep(1)
    q.put(None) # Poison pill

queue = Queue()
thread1 = Thread( target=thread1, args=("Thread-1", queue) )
thread2 = Thread( target=thread2, args=("Thread-2", queue) )

thread1.start()
thread2.start()
thread1.join()
thread2.join()

これは大きな問題を解決します。そして、それを回避するためのほぼ正しいアプローチのようです。
アビデモン2016年

これは、同期の問題を解決するために私が使用している方法です。
Zhang LongQI 2016年

1
いくつか質問があります。まず、スレッド間で共有する変数が複数ある場合、変数ごとに個別のキューが必要ですか?次に、上記のプログラムのキューが同期されるのはなぜですか?それぞれが各関数のローカルコピーとして機能するべきではありませんか?

これは古いですが、とにかく答えます。キュー自体は同期されておらず、変数のみが同期されていますa。同期を作成するのは、キューのデフォルトのブロック動作です。ステートメントa = q.get()は、値aが使用可能になるまでブロック(待機)します。変数qはローカルです。別の値を割り当てると、ローカルでのみ発生します。ただし、コードで割り当てられているキューは、メインスレッドで定義されているキューです。

1
スレッド間で情報情報を共有するために、必ずしもキューを使用する必要はありません。チェプナーの答えの例は完全に問題ありません。また、キューは必ずしも適切なツールではありません。キューは、たとえば、値が使用可能になるまでブロックする場合に役立ちます。2つのスレッドが共有リソースで競合する場合は役に立ちません。最後に、グローバル変数はスレッドで最悪ではありません。実際、それらはより自然なものになる可能性があります。たとえば、スレッドは、独自のプロセスを必要とするコードのブロック、たとえばループである可能性があります。したがって、ローカルスコープは、ループを関数に配置するときに人為的に作成されます。

5

などのロックの使用を検討する必要がありますthreading.Lock。詳細については、lock-objectsを参照してください。

受け入れられた答えはthread1で10を印刷できますが、これはあなたが望むものではありません。次のコードを実行すると、バグをより簡単に理解できます。

def thread1(threadname):
    while True:
      if a % 2 and not a % 2:
          print "unreachable."

def thread2(threadname):
    global a
    while True:
        a += 1

ロックを使用するとa、複数回の読み取り中にの変更を禁止できます。

def thread1(threadname):
    while True:
      lock_a.acquire()
      if a % 2 and not a % 2:
          print "unreachable."
      lock_a.release()

def thread2(threadname):
    global a
    while True:
        lock_a.acquire()
        a += 1
        lock_a.release()

変数を長時間使用するスレッドの場合は、最初にローカル変数に対処することをお勧めします。


3

その方法を提案してくれたJasonPanに感謝します。thread1 ifステートメントはアトミックではないため、そのステートメントの実行中に、thread2がthread1に侵入して、到達不能なコードに到達する可能性があります。以前の投稿のアイデアを、Python 2.7で実行した完全なデモンストレーションプログラム(以下)にまとめました。

いくつかの慎重な分析により、さらに洞察を得ることができると確信していますが、今のところ、非アトミックな動作がスレッド化と出会ったときに何が起こるかを示すことが重要だと思います。

# ThreadTest01.py - Demonstrates that if non-atomic actions on
# global variables are protected, task can intrude on each other.
from threading import Thread
import time

# global variable
a = 0; NN = 100

def thread1(threadname):
    while True:
      if a % 2 and not a % 2:
          print("unreachable.")
    # end of thread1

def thread2(threadname):
    global a
    for _ in range(NN):
        a += 1
        time.sleep(0.1)
    # end of thread2

thread1 = Thread(target=thread1, args=("Thread1",))
thread2 = Thread(target=thread2, args=("Thread2",))

thread1.start()
thread2.start()

thread2.join()
# end of ThreadTest01.py

予想どおり、この例を実行すると、「到達不能」コードに実際に到達して出力が生成されることがあります。

さらに、ロックの取得と解放のペアをthread1に挿入すると、「到達不能」メッセージが出力される可能性が大幅に低下することがわかりました。メッセージを表示するために、スリープ時間を0.01秒に短縮し、NNを1000に増やしました。

thread1にロックの取得と解放のペアがあるため、メッセージが表示されるとはまったく予想していませんでしたが、メッセージは表示されています。ロックの取得/解放のペアをthread2にも挿入した後、メッセージが表示されなくなりました。ヒンディー語では、thread2のincrementステートメントもおそらく非アトミックです。


1
これらは協調的な「アドバイザリロック」(「必須」ではない)であるため、両方のスレッドでロックが必要です。あなたは、インクリメントステートメントが非アトミックであるという点で正しいです。
Darkonaut

1

さて、実行例:

警告!自宅/職場では絶対にこれを行わないでください!教室でのみ;)

急いでいる状態を避けるために、セマフォ、共有変数などを使用してください。

from threading import Thread
import time

a = 0  # global variable


def thread1(threadname):
    global a
    for k in range(100):
        print("{} {}".format(threadname, a))
        time.sleep(0.1)
        if k == 5:
            a += 100


def thread2(threadname):
    global a
    for k in range(10):
        a += 1
        time.sleep(0.2)


thread1 = Thread(target=thread1, args=("Thread-1",))
thread2 = Thread(target=thread2, args=("Thread-2",))

thread1.start()
thread2.start()

thread1.join()
thread2.join()

および出力:

Thread-1 0
Thread-1 1
Thread-1 2
Thread-1 2
Thread-1 3
Thread-1 3
Thread-1 104
Thread-1 104
Thread-1 105
Thread-1 105
Thread-1 106
Thread-1 106
Thread-1 107
Thread-1 107
Thread-1 108
Thread-1 108
Thread-1 109
Thread-1 109
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110
Thread-1 110

タイミングが正しければ、a += 100操作はスキップされます。

プロセッサはTa+100で実行され、104を取得します。しかし、停止し、次のスレッドにジャンプします。ここで、At T + 1はa+1、古い値a、で実行されますa == 4。したがって、5を計算します。ジャンプバック(T + 2)、スレッド1、およびa=104メモリへの書き込み。スレッド2に戻ると、時間はT + 3でa=5、メモリに書き込みます。出来上がり!次の印刷命令は、104ではなく5を印刷します。

再現してキャッチする非常に厄介なバグ。


正しい実装を追加することも検討してください。これは、スレッド間でデータを共有することを学ぶ人にとって非常に役立ちます。
JS。

1
「todo」リストに追加:)
visoft19年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.