Pregunta

Hace poco escribí un programa que utiliza un simple patrón de productor / consumidor. En un principio tenía un error relacionado con el uso indebido de threading.Lock que al final lo arreglaron. Pero me hizo pensar si es posible implementar el patrón productor / consumidor de una manera lockless.

Requisitos en mi caso eran simples:

  • Un hilo productor.
  • Un hilo consumidor.
  • Cola tiene lugar solamente un artículo.
  • El productor puede producir orden del día, antes de que se consume la actual. Por lo tanto, el elemento actual se pierde, pero está bien para mí.
  • consumidor puede consumir elemento actual para que se produzca la siguiente. Por lo tanto, el elemento actual se consume dos veces (o más), pero está bien para mí.

Así que escribí esto:

QUEUE_ITEM = None

# this is executed in one threading.Thread object
def producer():
    global QUEUE_ITEM
    while True:
        i = produce_item()
        QUEUE_ITEM = i

# this is executed in another threading.Thread object
def consumer():
    global QUEUE_ITEM
    while True:
        i = QUEUE_ITEM
        consume_item(i)

Mi pregunta es: ¿Es este código flujos seguros

?

Comentario inmediata:. Este código no es realmente lockless - Yo uso CPython y tiene GIL

He probado el código un poco y parece que funciona. Se traduce en algunas operaciones de carga y almacenamiento que son atómica debido GIL. Pero también sé que del x operación no es atómico cuando x implementos __del__ método. Así que si mi artículo tiene un método y un poco de programación <=> desagradable ocurre, las cosas se pueden romper. O no?

Otra pregunta es: ¿Qué tipo de restricciones (por ejemplo sobre el tipo de artículos producidos) tengo que imponer para hacer lo anterior trabajo fino código

Mis preguntas son sólo alrededor posibilidad teórica para explotar las peculiaridades del GIL CPython y con el fin de llegar a lockless (es decir, no hay cerraduras como threading.Lock explícitamente en el código) solución.

¿Fue útil?

Solución

Si esto va a funcionar de la manera que se describe a continuación:

  1. que el productor puede producir un elemento que se puede omitir.
  2. Que el consumidor puede consumir el mismo elemento.
  

Pero también sé que el funcionamiento del x no es atómico cuando x implementos del método. Así que si mi artículo tiene un del método de programación y algunos desagradable ocurre, las cosas se pueden romper.

No veo un "del" aquí. Si una del sucede en consume_item entonces el del puede ocurrir en el hilo del fabricante. No creo que esto sería un "problema".

No trate de utilizar esto sin embargo. Usted va a terminar con un máximo de CPU en ciclos de interrogación sin sentido, y no es más rápido que el uso de una cola con las cerraduras desde Python ya tiene un bloqueo global.

Otros consejos

El engaño va a morder. Sólo tiene que utilizar cola para la comunicación entre hilos.

Esto no es realmente hilo seguro porque productor podría sobrescribir QUEUE_ITEM antes consumidor ha consumido y el consumidor podría consumir <=> dos veces. Como usted ha mencionado, que estás bien con eso, pero la mayoría de la gente no lo son.

Una persona con más conocimiento del funcionamiento interno de CPython tendrá que responder a las preguntas que más teóricos.

Creo que es posible que un hilo se interrumpe mientras que la producción / lento, especialmente si los artículos son grandes objetos. Editar: esto es sólo una suposición. No soy un experto.

También los hilos pueden producir / consumir cualquier cantidad de artículos antes de que el otro se pone en marcha.

Puede utilizar una lista como la cola, siempre y cuando se apegue a anexar / pop ya que ambos son atómica.

QUEUE = []

# this is executed in one threading.Thread object
def producer():
    global QUEUE
    while True:
        i = produce_item()
        QUEUE.append(i)

# this is executed in another threading.Thread object
def consumer():
    global QUEUE
    while True:
        try:
            i = QUEUE.pop(0)
        except IndexError:
            # queue is empty
            continue

        consume_item(i)

En un ámbito de clase, como a continuación, incluso se puede borrar la cola.

class Atomic(object):
    def __init__(self):
        self.queue = []

    # this is executed in one threading.Thread object
    def producer(self):
        while True:
            i = produce_item()
            self.queue.append(i)

    # this is executed in another threading.Thread object
    def consumer(self):
        while True:
            try:
                i = self.queue.pop(0)
            except IndexError:
                # queue is empty
                continue

            consume_item(i)

    # There's the possibility producer is still working on it's current item.
    def clear_queue(self):
        self.queue = []

Vas a tener que averiguar qué operaciones de lista son atómicas mirando el código de bytes generada.

El __del__ podría ser un problema como usted ha dicho. Podría ser evitado, aunque sólo había una manera de evitar que el recolector de basura de invocar el método QUEUE_ITEM en el antiguo objeto antes de terminar la asignación de la nueva para el <=>. Necesitaríamos algo como:

increase the reference counter on the old object
assign a new one to `QUEUE_ITEM`
decrease the reference counter on the old object

Me temo, no sé si es posible, sin embargo.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top