循环中扭曲等待事件

时间:2014-12-25 11:01:09

标签: python twisted

我想从外部服务中读取和处理一些数据。我问服务是否有任何数据,如果有什么东西返回我处理它并再次询问(因此数据可以在它可用时立即处理),否则我等待数据可用的通知。这可以写成无限循环:

def loop(self):
    while True:
        data = yield self.get_data_nonblocking()
        if data is not None:
            yield self.process_data(data)
        else:
            yield self.data_available

def on_data_available(self):
    self.data_available.fire()

如何data_available在这里实施?它可能是延迟但延迟不能重置,只能重新创建。有更好的选择吗?

这个循环可以集成到Twisted事件循环中吗?我可以在on_data_available中读取和处理数据并编写一些代码而不是循环检查get_data_nonblocking但我觉得我需要一些锁以确保数据以相同的顺序处理它到了(上面的代码强制执行它,因为它是它处理的唯一地方)。这根本不是一个好主意吗?

1 个答案:

答案 0 :(得分:3)

考虑TCP连接的情况。 TCP连接的接收缓冲区可以包含数据,也可以不包含数据。您可以通过使用非阻塞套接字API来获取该数据,或者什么都不做,而不会阻塞:

data = socket.recv(1024)
if data:
    self.process_data(data)

您可以使用select()(或任何基本等效的API)等待数据可用:

socket.setblocking(False)
while True:
    data = socket.recv(1024)
    if data:
        self.process_data(data)
    else:
        select([socket], [], [])

其中,只有select() 特别 Twisted-unndlyndly(虽然Twisted成语肯定不会自己进行socket.recv次调用)。您可以将select调用替换为适合扭曲的版本(使用Protocol方法实施dataReceived来触发Deferred - 有点像on_data_available方法 - 抛出一些产量并使整个事件成为inlineCallbacks生成器。

但是,虽然这是从TCP连接获取数据的一种方式,但这并不是Twisted鼓励您使用的API。相反,API是:

class SomeProtocol(Protocol):
    def dataReceived(self, data):
        # Your logic here

我不知道你的情况有多大不同。如果你做了类似的事情而不是你写的循环,那该怎么办:

class YourDataProcessor(object):
    def process_data(self, data):
        # Your logic here

class SomeDataGetter(object):
    def __init__(self, processor):
        self.processor = processor

    def on_available_data(self):
        data = self.get_data_nonblocking()
        if data is not None:
            self.processor.process_data(data)

现在根本没有延迟(除了可能在任何实现on_available_dataget_data_nonblocking中,但我看不到该代码)。

如果你粗略地保留这个,你可以保证按顺序执行,因为Twisted是单线程的(除了在几个非常清楚标记的地方),并且在单线程程序中,之前的调用是process_data必须在以后调用process_data之前完成{当然,process_data重新调用自身的情况除外 - 但这是另一个故事。)

如果你将其切换回使用inlineCallbacks(或任何等效的“coroutine”风味饮料混合物),那么你可能会引入无序执行的可能性。

例如,如果get_data_nonblocking返回Deferred并且你写了这样的内容:

    @inlineCallbacks
    def on_available_data(self):
        data = yield self.get_data_nonblocking()
        if data is not None:
            self.processor.process_data(data)

然后您已更改on_available_data以表示在调用get_data_nonblocking时允许上下文切换。在这种情况下,根据您对get_data_nonblockingon_available_data的实施情况,完全有可能:

  1. on_available_data被称为
  2. get_data_nonblocking被调用并返回Deferred
  3. on_available_data告诉执行切换到另一个上下文(通过yield / inlineCallbacks
  4. 再次调用
  5. on_available_data
  6. 再次调用
  7. get_data_nonblocking并返回Deferred(也许是同一个!也许是新的!取决于它是如何实现的)
  8. on_available_data的第二次调用告诉执行切换到另一个上下文(同样的原因)
  9. 反应堆旋转了一段时间,最终一个事件到来,导致Deferred第二次调用返回的get_data_nonblocking被触发。
  10. 执行切换回第二个 on_available_data框架
  11. 使用第二次 process_data调用返回的任何数据调用
  12. get_data_nonblocking
  13. 最终对第一组对象发生同样的事情,并且使用第一次 process_data调用返回的任何数据再次调用get_data_nonblocking
  14. 现在也许您已经无序处理了数据 - 再次,这取决于系统其他部分的更多细节。

    如果是这样,您可以随时重新下订单。对此有很多不同的可行方法。 Twisted本身没有任何明确支持此操作的API,因此解决方案涉及编写一些新代码。这是一个方法的一个想法(未经测试) - 一个类似队列的类,它知道对象序列号:

    class SequencedQueue(object):
        """
        A queue-like type which guarantees objects come out of the queue in the order
        defined by a sequence number associated with the objects when they are put into
        the queue.
    
        Application code manages sequence number assignment so that sequence numbers don't
        have to have the same order as `put` calls on this type.
        """
        def __init__(self):
            # The sequence number of the object that should be given out
            # by the next call to `get`
            self._next_sequence = 0
    
            # The sequence number of the next result that needs to be provided.
            self._next_result = 0
    
            # A holding area for objects past _next_sequence
            self._queue = {}
    
            # A holding area 
            self._waiting = 
    
        def put(self, sequence, object):
            """
            Put an object into the queue at a particular point in the sequence.
            """
            if sequence < self._next_sequence:
                # Programming error.  The sequence number
                # of the object being put has already been used.
                raise ...
    
            self._queue[sequence] = object
            self._check_waiters()
    
        def get(self):
            """
            Get an object from the queue which has the next sequence number
            following whatever was previously gotten.
            """
            result = self._waiters[self._next_sequence] = Deferred()
            self._next_sequence += 1
            self._check_waiters()
            return result
    
        def _check_waiters(self):
            """
            Find any Deferreds previously given out by get calls which can now be given
            their results and give them to them.
            """
            while True:
                seq = self._next_result
                if seq in self._queue and seq in self._waiting:
                    self._next_result += 1
                    # XXX Probably a re-entrancy bug here.  If a callback calls back in to
                    # put then this loop might run recursively
                    self._waiting.pop(seq).callback(self._queue.pop(seq))
                else:
                    break
    

    预期的行为(模块化我意外添加的任何错误)类似于:

    q = SequencedQueue()
    d1 = q.get()
    d2 = q.get()
    # Nothing in particular happens
    q.put(1, "second result")
    # d1 fires with "first result" and afterwards d2 fires with "second result"
    q.put(0, "first result")
    

    使用此功能,只需确保按照您希望分派数据的顺序分配序列号,而不是按照实际显示的顺序分配序列号。例如:

        @inlineCallbacks
        def on_available_data(self):
            sequence = self._process_order
            data = yield self.get_data_nonblocking()
            if data is not None:
                self._process_order += 1
                self.sequenced_queue.put(sequence, data)
    

    在其他地方,某些代码可以使用类似的队列类型:

    @inlineCallbacks
    def queue_consumer(self):
        while True:
            yield self.process_data(yield self.sequenced_queue.get())