繁体   English   中英

在队列上复用.Queue?

[英]Multiplex on queue.Queue?

如何在多个queue.Queue同时“选择” queue.Queue同时进行?

Golang的频道具有所需的功能

select {
case i1 = <-c1:
    print("received ", i1, " from c1\n")
case c2 <- i2:
    print("sent ", i2, " to c2\n")
case i3, ok := (<-c3):  // same as: i3, ok := <-c3
    if ok {
        print("received ", i3, " from c3\n")
    } else {
        print("c3 is closed\n")
    }
default:
    print("no communication\n")
}

其中,要解锁的第一个通道执行相应的块。 我将如何在Python中实现这一目标?

Update0

根据tux21b的答案中给出的链接 ,所需的队列类型具有以下属性:

  • 多生产者/多消费者队列(MPMC)
  • 提供每个生产者的FIFO / LIFO
  • 当队列为空/完全消费者/生产者被阻止时

此外,渠道可以阻塞,生产者将阻止,直到消费者检索该项目。 我不确定Python的Queue可以做到这一点。

如果使用queue.PriorityQueue ,则可以使用通道对象作为优先级获得类似的行为:

import threading, logging
import random, string, time
from queue import PriorityQueue, Empty
from contextlib import contextmanager

logging.basicConfig(level=logging.NOTSET,
                    format="%(threadName)s - %(message)s")

class ChannelManager(object):
    next_priority = 0

    def __init__(self):
        self.queue = PriorityQueue()
        self.channels = []

    def put(self, channel, item, *args, **kwargs):
        self.queue.put((channel, item), *args, **kwargs)

    def get(self, *args, **kwargs):
        return self.queue.get(*args, **kwargs)

    @contextmanager
    def select(self, ordering=None, default=False):
        if default:
            try:
                channel, item = self.get(block=False)
            except Empty:
                channel = 'default'
                item = None
        else:
            channel, item = self.get()
        yield channel, item


    def new_channel(self, name):
        channel = Channel(name, self.next_priority, self)
        self.channels.append(channel)
        self.next_priority += 1
        return channel


class Channel(object):
    def __init__(self, name, priority, manager):
        self.name = name
        self.priority = priority
        self.manager = manager

    def __str__(self):
        return self.name

    def __lt__(self, other):
        return self.priority < other.priority

    def put(self, item):
        self.manager.put(self, item)


if __name__ == '__main__':
    num_channels = 3
    num_producers = 4
    num_items_per_producer = 2
    num_consumers = 3
    num_items_per_consumer = 3

    manager = ChannelManager()
    channels = [manager.new_channel('Channel#{0}'.format(i))
                for i in range(num_channels)]

    def producer_target():
        for i in range(num_items_per_producer):
            time.sleep(random.random())
            channel = random.choice(channels)
            message = random.choice(string.ascii_letters)
            logging.info('Putting {0} in {1}'.format(message, channel))
            channel.put(message)

    producers = [threading.Thread(target=producer_target,
                                  name='Producer#{0}'.format(i))
                 for i in range(num_producers)]
    for producer in producers:
        producer.start()
    for producer in producers:
        producer.join()
    logging.info('Producers finished')

    def consumer_target():
        for i in range(num_items_per_consumer):
            time.sleep(random.random())
            with manager.select(default=True) as (channel, item):
                if channel:
                    logging.info('Received {0} from {1}'.format(item, channel))
                else:
                    logging.info('No data received')

    consumers = [threading.Thread(target=consumer_target,
                                  name='Consumer#{0}'.format(i))
                 for i in range(num_consumers)]
    for consumer in consumers:
        consumer.start()
    for consumer in consumers:
        consumer.join()
    logging.info('Consumers finished')

示例输出:

Producer#0 - Putting x in Channel#2
Producer#2 - Putting l in Channel#0
Producer#2 - Putting A in Channel#2
Producer#3 - Putting c in Channel#0
Producer#3 - Putting z in Channel#1
Producer#1 - Putting I in Channel#1
Producer#1 - Putting L in Channel#1
Producer#0 - Putting g in Channel#1
MainThread - Producers finished
Consumer#1 - Received c from Channel#0
Consumer#2 - Received l from Channel#0
Consumer#0 - Received I from Channel#1
Consumer#0 - Received L from Channel#1
Consumer#2 - Received g from Channel#1
Consumer#1 - Received z from Channel#1
Consumer#0 - Received A from Channel#2
Consumer#1 - Received x from Channel#2
Consumer#2 - Received None from default
MainThread - Consumers finished

在此示例中, ChannelManager只是queue.PriorityQueue的包装器, queue.PriorityQueue select方法实现为contextmanager ,使其看起来类似于Go中的select语句。

有几点需要注意:

  • 订购

    • 在Go示例中,在select语句中写入通道的顺序决定了如果有多个通道可用的数据,将执行哪个通道的代码。

    • 在python示例中,顺序由分配给每个通道的优先级确定。 但是,可以将优先级分配给每个通道(如示例所示),因此可以使用更复杂的select方法来更改顺序,该方法负责根据方法的参数分配新的优先级。 此外,一旦上下文管理器完成,就可以重新建立旧的排序。

  • 闭塞

    • 在Go示例中,如果存在default情况,则select语句将被阻止。

    • 在python示例中,必须将boolean参数传递给select方法,以便在需要阻塞/非阻塞时使其清晰。 在非阻塞情况下,上下文管理器返回的通道只是字符串'default'因此在代码里面很容易在with语句内的代码中检测到它。

  • 线程: queue模块中的对象已经为多生产者,多消费者场景做好了准备,如示例中所示。

pychan项目在Python中复制Go通道,包括多路复用。 它实现了与Go相同的算法,因此它满足您所有需要的属性:

  • 多个生产者和消费者可以通过Chan进行交流。 当生产者和消费者都准备好时,他们将阻止
  • 生产者和消费者按他们到达的顺序(FIFO)进行维修
  • 空(完整)队列将阻止消费者(生产者)。

这是你的例子的样子:

c1 = Chan(); c2 = Chan(); c3 = Chan()

try:
    chan, value = chanselect([c1, c3], [(c2, i2)])
    if chan == c1:
        print("Received %r from c1" % value)
    elif chan == c2:
        print("Sent %r to c2" % i2)
    else:  # c3
        print("Received %r from c3" % value)
except ChanClosed as ex:
    if ex.which == c3:
        print("c3 is closed")
    else:
        raise

(完全披露:我写了这个库)

生产者 - 消费者队列有许多不同的实现,例如queue.Queue可用。 他们通常在Dmitry Vyukov的这篇优秀文章中列出的很多属性上都有所不同。 如您所见,有超过10k种不同的组合可能。 用于这种队列的算法也根据要求而有很大不同。 仅扩展现有队列算法以保证其他属性是不可能的,因为这通常需要不同的内部数据结构和不同的算法。

Go的频道提供了相对较多的保证属性,因此这些频道可能适合很多节目。 最困难的要求之一是支持一次读取/阻止多个通道(select语句),并且如果select语句中有多个分支能够继续,则公平地选择一个通道,这样就不会留下任何消息。 Python的queue.Queue不提供此功能,因此无法使用它存档相同的行为。

因此,如果要继续使用queue.Queue ,则需要找到该问题的解决方法。 然而,解决方法有自己的缺点列表,并且难以维护。 寻找另一个提供所需功能的生产者 - 消费者队列可能是一个更好的主意! 无论如何,这里有两种可能的解决方法:

轮询

while True:
  try:
    i1 = c1.get_nowait()
    print "received %s from c1" % i1
  except queue.Empty:
    pass
  try:
    i2 = c2.get_nowait()
    print "received %s from c2" % i2
  except queue.Empty:
    pass
  time.sleep(0.1)

轮询频道时可能会占用大量CPU周期,而当有大量消息时可能会很慢。 使用带有指数退避时间的time.sleep()(而不是此处显示的常数0.1秒)可能会大大改善此版本。

单个通知队列

queue_id = notify.get()
if queue_id == 1:
  i1 = c1.get()
  print "received %s from c1" % i1
elif queue_id == 2:
  i2 = c2.get()
  print "received %s from c2" % i2

使用此设置,您必须在发送到c1或c2后向通知队列发送一些内容。 这可能适合您,只要只有一个这样的通知队列就足够了(即您没有多个“选择”,每个都阻塞在您的频道的不同子集上)。

或者您也可以考虑使用Go。 Go的goroutines和并发支持比Python的有限线程功能强大得多。

from queue import Queue

# these imports needed for example code
from threading import Thread
from time import sleep
from random import randint

class MultiQueue(Queue):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.queues = []

    def addQueue(self, queue):
        queue.put = self._put_notify(queue, queue.put)
        queue.put_nowait = self._put_notify(queue, queue.put_nowait)
        self.queues.append(queue)

    def _put_notify(self, queue, old_put):
        def wrapper(*args, **kwargs):
            result = old_put(*args, **kwargs)
            self.put(queue)
            return result
        return wrapper

if __name__ == '__main__':
    # an example of MultiQueue usage

    q1 = Queue()
    q1.name = 'q1'
    q2 = Queue()
    q2.name = 'q2'
    q3 = Queue()
    q3.name = 'q3'

    mq = MultiQueue()
    mq.addQueue(q1)
    mq.addQueue(q2)
    mq.addQueue(q3)

    queues = [q1, q2, q3]
    for i in range(9):
        def message(i=i):
            print("thread-%d starting..." % i)
            sleep(randint(1, 9))
            q = queues[i%3]
            q.put('thread-%d ending...' % i)
        Thread(target=message).start()

    print('awaiting results...')
    for _ in range(9):
        result = mq.get()
        print(result.name)
        print(result.get())

与其尝试使用多个队列的.get()方法不同,这里的想法是让队列在数据就绪时通知MultiQueue - 反向select 这是通过让MultiQueue包装各种Queueput()put_nowait()方法来实现的,这样当某些东西被添加到这些队列时,该队列就会被put() MultiQueue和相应的MultiQueue.get()将检索已准备好数据的Queue

MultiQueue基于FIFO队列,但您也可以根据需要使用LIFO或优先级队列作为基础。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM