web-dev-qa-db-ja.com

ジェネレーターはスレッドセーフですか?

ジェネレーター関数を作成し、それを新しいスレッドに渡すマルチスレッドプログラムがあります。各スレッドがジェネレーターから次の値を取得できるように、本質的に共有/グローバルにする必要があります。

このようなジェネレーターを使用しても安全ですか、それとも複数のスレッドから共有ジェネレーターにアクセスする際に問題や条件が発生しますか?

そうでない場合、問題に取り組むためのより良い方法はありますか?リストを循環し、それを呼び出すスレッドに対して次の値を生成するものが必要です。

43
Corey Goldberg

スレッドセーフではありません。同時呼び出しはインターリーブし、ローカル変数を混乱させる可能性があります。

一般的なアプローチは、マスタースレーブパターン(現在、PCではファーマーワーカーパターンと呼ばれています)を使用することです。データを生成する3番目のスレッドを作成し、マスターとスレーブの間にキューを追加します。ここで、スレーブはキューから読み取り、マスターはキューに書き込みます。標準のキューモジュールは、必要なスレッドセーフを提供し、スレーブがさらにデータを読み取る準備ができるまでマスターをブロックするように調整します。

55

以下のベンチマークを追加するために編集されました。

ジェネレーターをロックでラップできます。例えば、

import threading
class LockedIterator(object):
    def __init__(self, it):
        self.lock = threading.Lock()
        self.it = it.__iter__()

    def __iter__(self): return self

    def next(self):
        self.lock.acquire()
        try:
            return self.it.next()
        finally:
            self.lock.release()

gen = [x*2 for x in [1,2,3,4]]
g2 = LockedIterator(gen)
print list(g2)

私のシステムではロックに50ミリ秒かかり、キューには350ミリ秒かかります。キューは、実際にキューがある場合に役立ちます。たとえば、着信HTTPリクエストがあり、ワーカースレッドによる処理のためにそれらをキューに入れたい場合です。 (これはPythonイテレーターモデルに適合しません。イテレーターがアイテムを使い果たすと、完了します。)本当にイテレーターがある場合は、LockedIteratorの方が高速で簡単です。スレッドセーフにするため。

from datetime import datetime
import threading
num_worker_threads = 4

class LockedIterator(object):
    def __init__(self, it):
        self.lock = threading.Lock()
        self.it = it.__iter__()

    def __iter__(self): return self

    def next(self):
        self.lock.acquire()
        try:
            return self.it.next()
        finally:
            self.lock.release()

def test_locked(it):
    it = LockedIterator(it)
    def worker():
        try:
            for i in it:
                pass
        except Exception, e:
            print e
            raise

    threads = []
    for i in range(num_worker_threads):
        t = threading.Thread(target=worker)
        threads.append(t)
        t.start()

    for t in threads:
        t.join()

def test_queue(it):
    from Queue import Queue
    def worker():
        try:
            while True:
                item = q.get()
                q.task_done()
        except Exception, e:
            print e
            raise

    q = Queue()
    for i in range(num_worker_threads):
         t = threading.Thread(target=worker)
         t.setDaemon(True)
         t.start()

    t1 = datetime.now()

    for item in it:
        q.put(item)

    q.join()

start_time = datetime.now()
it = [x*2 for x in range(1,10000)]

test_locked(it)
#test_queue(it)
end_time = datetime.now()
took = end_time-start_time
print "took %.01f" % ((took.seconds + took.microseconds/1000000.0)*1000)
46
Glenn Maynard

いいえ、スレッドセーフではありません。ジェネレーターとマルチスレッドに関する興味深い情報は、次の場所にあります。

http://www.dabeaz.com/generators/Generators.pdf

6