Zijn generatoren draadveilig?

Ik heb een programma met meerdere threads waarin ik een generatorfunctie maak en deze vervolgens doorgeef aan nieuwe threads. Ik wil dat het gedeeld/wereldwijd van aard is, zodat elke thread de volgende waarde van de generator kan krijgen.

Is het veilig om een ​​generator als deze te gebruiken, of loop ik tegen problemen/voorwaarden aan bij het benaderen van de gedeelde generator vanuit meerdere threads?

Zo niet, is er een betere manier om het probleem aan te pakken? Ik heb iets nodig dat door een lijst gaat en de volgende waarde produceert voor welke thread het ook aanroept.


Antwoord 1, autoriteit 100%

Het is niet thread-safe; gelijktijdige oproepen kunnen elkaar verwisselen en knoeien met de lokale variabelen.

De gebruikelijke benadering is om het master-slave-patroon te gebruiken (nu boer-werker-patroon genoemd op pc). Maak een derde thread die gegevens genereert en voeg een wachtrij toe tussen de master en de slaves, waar slaves uit de wachtrij zullen lezen en de master ernaar zal schrijven. De standaard wachtrijmodule zorgt voor de nodige threadbeveiliging en zorgt ervoor dat de master wordt geblokkeerd totdat de slaves klaar zijn om meer gegevens te lezen.


Antwoord 2, autoriteit 84%

Bewerkt om hieronder benchmark toe te voegen.

Je kunt een generator omwikkelen met een slot. Bijvoorbeeld,

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)

Vergrendelen duurt 50 ms op mijn systeem, wachtrij duurt 350 ms. Wachtrij is handig als je echt een wachtrij hebt; als u bijvoorbeeld inkomende HTTP-verzoeken hebt en deze in de wachtrij wilt plaatsen voor verwerking door werkthreads. (Dat past niet in het Python-iteratormodel – zodra een iterator geen items meer heeft, is het klaar.) Als je echt een iterator hebt, dan is LockedIterator een snellere en eenvoudigere manier om het thread-safe te maken.

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)

Antwoord 3, autoriteit 8%

Nee, ze zijn niet draadveilig. U kunt interessante informatie over generatoren en multi-threading vinden in:

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


Antwoord 4, autoriteit 2%

Het generatorobject zelf is draadveilig als elk pyobject beschermd door de GIL. Maar de thread probeert het volgende element uit de generator te krijgen dat al in uitvoeringstoestand is in een andere thread (het uitvoeren van de generatorcode tussen de yield‘s) zou waardeerror krijgen:

ValueError: generator already executing

Voorbeeldcode:

from threading import Thread
from time import sleep
def gen():
    sleep(1)
    yield
g = gen()
Thread(target=g.__next__).start()
Thread(target=g.__next__).start()

Resultaten in:

Exception in thread Thread-2:
Traceback (most recent call last):
  File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner
    self.run()
  File "/usr/lib/python3.8/threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
ValueError: generator already executing

Maar eigenlijk is dit niet gerelateerd aan threading überhaupt. En kan worden gereproduceerd in een enkele draad:

def gen():
    yield next(g)
g = gen()
next(g)

Antwoord 5, Autoriteit 2%

Courtesy van IIRC Python Freenode, hier is een werkoplossingen voor Python 3.x

Generatoren zijn standaard geen draad veilig, maar hier is hoe ze ze moeten maken om draad veilig te zijn

def my_generator():
    while True:
        for x in range(10):
            yield x

class LockedIterator(object):
    def __init__(self, it):
        self._lock = threading.Lock()
        self._it = iter(it)
    def __iter__(self):
        return self
    def __next__(self):
        with self._lock:
            return next(self._it)
n = LockedIterator(my_generator)
next(n)
next(n)
next(n)

of gebruik een functie

def threadsafe_iter(iterable):
    lock = threading.Lock()
    iterator = iter(iterable)
    while True:
        with lock:
            for value in iterator:
                break
            else:
                return
        yield value
n = threadsafe_iter(my_generator)
next(n)
next(n)
next(n)

Antwoord 6

Het hangt af van welke Python-implementatie u gebruikt. In CPYTHON maakt de GIL alle bewerkingen op Python-objecten-threadsafe, omdat slechts één draad op een bepaald moment code kan uitvoeren.

http://en.wikipedia.org/wiki/global_interpreter_lock

Other episodes