[英]Consumer/Producer “timely” queue
我已經實現了消費者/生產者優先級隊列,其中優先級實際上是一個時間戳,代表何時應交付該項目。 它工作得很好,但是我想知道是否有人有更好的想法來實現這一點或對當前實現發表評論。
該代碼在Python中。 創建了一個線程來按時喚醒等待中的使用者。 我知道這是在庫中創建線程的反模式,但是我無法設計其他方法。
這是代碼:
import collections
import heapq
import threading
import time
class TimelyQueue(threading.Thread):
"""
Implements a similar but stripped down interface of Queue which
delivers items on time only.
"""
class Locker:
def __init__(self, lock):
self.l = lock
def __enter__(self):
self.l.acquire()
return self.l
def __exit__(self, type, value, traceback):
self.l.release()
# Optimization to avoid wasting CPU cycles when something
# is about to happen in less than 5 ms.
_RESOLUTION = 0.005
def __init__(self):
threading.Thread.__init__(self)
self.daemon = True
self.queue = []
self.triggered = collections.deque()
self.putcond = threading.Condition()
self.getcond = threading.Condition()
# Optimization to avoid waking the thread uselessly.
self.putwaketime = 0
def put(self, when, item):
with self.Locker(self.putcond):
heapq.heappush(self.queue, (when, item))
if when < self.putwaketime or self.putwaketime == 0:
self.putcond.notify()
def get(self, timeout=None):
with self.Locker(self.getcond):
if len(self.triggered) > 0:
when, item = self.triggered.popleft()
return item
self.getcond.wait(timeout)
try:
when, item = self.triggered.popleft()
except IndexError:
return None
return item
def qsize(self):
with self.Locker(self.putcond):
return len(self.queue)
def run(self):
with self.Locker(self.putcond):
maxwait = None
while True:
curtime = time.time()
try:
when, item = self.queue[0]
maxwait = when - curtime
self.putwaketime = when
except IndexError:
maxwait = None
self.putwaketime = 0
self.putcond.wait(maxwait)
curtime = time.time()
while True:
# Don't dequeue now, we are not sure to use it yet.
try:
when, item = self.queue[0]
except IndexError:
break
if when > curtime + self._RESOLUTION:
break
self.triggered.append(heapq.heappop(self.queue))
if len(self.triggered) > 0:
with self.Locker(self.getcond):
self.getcond.notify()
if __name__ == "__main__":
q = TimelyQueue()
q.start()
N = 50000
t0 = time.time()
for i in range(N):
q.put(time.time() + 2, i)
dt = time.time() - t0
print "put done in %.3fs (%.2f put/sec)" % (dt, N / dt)
t0 = time.time()
i = 0
while i < N:
a = q.get(3)
if i == 0:
dt = time.time() - t0
print "start get after %.3fs" % dt
t0 = time.time()
i += 1
dt = time.time() - t0
print "get done in %.3fs (%.2f get/sec)" % (dt, N / dt)
您真正需要的唯一的背景線程是一個計時器,當它用完時,它會踢服務員,對嗎?
首先,可以使用threading.Timer
而不是顯式的后台線程來實現。 但是,盡管這可能更簡單,但它並不能真正解決您在用戶背后創建線程的問題,無論他們是否想要。 同樣,使用threading.Timer
,實際上每次重新啟動計時器時都會分離出一個新線程,這可能是性能問題。 (一次只能有一個線程,但是啟動和停止線程並不是免費的。)
如果您查看PyPI模塊,ActiveState配方和各種框架,那么有許多實現可讓您在單個后台線程上運行多個計時器。 那將解決您的問題。
但這仍然不是一個完美的解決方案。 例如,假設我的應用需要20個TimelyQueue
對象-或一個TimelyQueue
對象,以及19個其他都需要計時器的東西。 我仍然會得到20個線程。 或者說我正在構建一個套接字服務器或GUI應用程序( TimelyQueue
的兩個最明顯的用例;我可以在事件循環的頂部實現一個計時器(或者,很可能僅使用附帶的計時器)框架),那么為什么我根本不需要線程?
解決的辦法是提供一個掛鈎來提供任何計時器工廠:
def __init__(self, timerfactory = threading.Timer):
self.timerfactory = timerfactory
...
現在,當您需要調整計時器時:
if when < self.waketime:
self.timer.cancel()
self.timer = self.timerfactory(when - now(), self.timercallback)
self.waketime = when
對於快速和骯臟的用例,這是開箱即用的好方法。 但是,例如,如果我使用twisted
,那么我可以使用TimelyQueue(twisted.reactor.callLater)
,現在隊列的計時器將通過twisted
事件循環。 或者,如果我有一個多計時器單線程實現,我在其他地方使用了TimelyQueue(multiTimer.add)
,現在隊列的計時器與我所有其他計時器都在同一線程上。
如果願意,您可以提供比threading.Timer
更好的默認值,但實際上,我認為大多數需要比threading.Timer
更好的東西的人都可以為自己的特定應用程序提供比您提供的任何東西更好的東西。
當然,並非每個計時器實現都具有與threading.Timer
相同的API-盡管您會驚訝地發現其中有很多功能。 但是編寫適配器並不難,如果您有要與TimelyQueue
一起使用的計時器,但其接口錯誤。 例如,如果我正在構建PyQt4 / PySide應用程序,則QTimer
沒有cancel
方法,並且需要ms而不是秒,因此我必須執行以下操作:
class AdaptedQTimer(object):
def __init__(self, timeout, callback):
self.timer = QTimer.singleShot(timeout * 1000, callback)
def cancel(self):
self.timer.stop()
q = TimelyQueue(AdaptedQTimer)
或者,如果我想更直接地將隊列集成到QObject
,則可以包裝QObject.startTimer()
並讓我的timerEvent(self)
方法調用回調。
一旦考慮適配器,最后一個想法。 我認為這不值得,但可能值得考慮。 如果你的計時器采取了時間戳,而不是timedelta,並具有adjust
方法,而不是/而不是cancel
,並舉行了自己的waketime
,你TimelyQueue
實現可以是簡單的,並且可能更有效。 在put
,你有這樣的事情:
if self.timer is None:
self.timer = self.timerfactory(when)
elif when < self.timer.waketime:
self.timer.adjust(when)
當然,大多數計時器都不提供此接口。 但是,如果有人擁有一個,或者願意制作一個,那么他們可以獲得好處。 對於其他所有人,您可以提供一個簡單的適配器,將一個threading.Timer
樣式的計時器轉換為所需的類型,例如:
def timerFactoryAdapter(threadingStyleTimerFactory):
class TimerFactory(object):
def __init__(self, timestamp, callback):
self.timer = threadingStyleTimerFactory(timestamp - now(), callback)
self.callback = callback
def cancel(self):
return self.timer.cancel()
def adjust(self, timestamp):
self.timer.cancel()
self.timer = threadingStyleTimerFactory(timestamp - now(), self.callback)
作為記錄,我已經使用計時器工廠實現了您的建議。 我使用上述版本運行了一個小型基准,並使用threading.Timer
類運行了新版本:
第一次實施
使用默認分辨率(5毫秒,即5毫秒窗口中的所有內容一起觸發),它可以實現約88k put()
/ sec和69k get()
/ sec。
將分辨率設置為0 ms(無優化)時,它可以達到約88k put()
/ sec和55k get()
/ sec。
第二實施
使用默認分辨率(5毫秒),它可以達到約88k put()
/ sec和65k get()
/ sec。
將分辨率設置為0 ms,它可以達到約88k put()
/ sec和62k get()
/ sec。
我承認我很驚訝沒有分辨率優化的第二種實現會更快。 現在進行調查為時已晚。
import collections
import heapq
import threading
import time
class TimelyQueue:
"""
Implements a similar but stripped down interface of Queue which
delivers items on time only.
"""
def __init__(self, resolution=5, timerfactory=threading.Timer):
"""
`resolution' is an optimization to avoid wasting CPU cycles when
something is about to happen in less than X ms.
"""
self.resolution = float(resolution) / 1000
self.timerfactory = timerfactory
self.queue = []
self.triggered = collections.deque()
self.putcond = threading.Condition()
self.getcond = threading.Condition()
# Optimization to avoid waking the thread uselessly.
self.putwaketime = 0
self.timer = None
self.terminating = False
def __arm(self):
"""
Arm the next timer; putcond must be acquired!
"""
curtime = time.time()
when, item = self.queue[0]
interval = when - curtime
self.putwaketime = when
self.timer = self.timerfactory(interval, self.__fire)
self.timer.start()
def __fire(self):
with self.putcond:
curtime = time.time()
debug = 0
while True:
# Don't dequeue now, we are not sure to use it yet.
try:
when, item = self.queue[0]
except IndexError:
break
if when > curtime + self.resolution:
break
debug += 1
self.triggered.append(heapq.heappop(self.queue))
if len(self.triggered) > 0:
with self.getcond:
self.getcond.notify(len(self.triggered))
if self.terminating:
return
if len(self.queue) > 0:
self.__arm()
def put(self, when, item):
"""
`when' is a Unix time from Epoch.
"""
with self.putcond:
heapq.heappush(self.queue, (when, item))
if when >= self.putwaketime and self.putwaketime != 0:
return
# Arm next timer.
if self.timer is not None:
self.timer.cancel()
self.__arm()
def get(self, timeout=None):
"""
Timely return the next object on the queue.
"""
with self.getcond:
if len(self.triggered) > 0:
when, item = self.triggered.popleft()
return item
self.getcond.wait(timeout)
try:
when, item = self.triggered.popleft()
except IndexError:
return None
return item
def qsize(self):
"""
Self explanatory.
"""
with self.putcond:
return len(self.queue)
def terminate(self):
"""
Request the embedded thread to terminate.
"""
with self.putcond:
self.terminating = True
if self.timer is not None:
self.timer.cancel()
self.putcond.notifyAll()
if __name__ == "__main__":
q = TimelyQueue(0)
N = 100000
t0 = time.time()
for i in range(N):
q.put(time.time() + 2, i)
dt = time.time() - t0
print "put done in %.3fs (%.2f put/sec)" % (dt, N / dt)
t0 = time.time()
i = 0
while i < N:
a = q.get(3)
if i == 0:
dt = time.time() - t0
print "start get after %.3fs" % dt
t0 = time.time()
i += 1
dt = time.time() - t0
print "get done in %.3fs (%.2f get/sec)" % (dt, N / dt)
q.terminate()
# Give change to the thread to exit properly, otherwise we may get
# a stray interpreter exception.
time.sleep(0.1)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.