在Python中实现Timer

时间:2013-06-05 20:47:45

标签: python django timer django-celery celerybeat

概述

  • 我有中等规模的django项目
  • 我在内存中有一堆前缀树(而不是DB)
  • 这些树的节点表示超时的实体/对象。即,我需要在不同的时间点超时这些节点

设计

  • 本质上,我需要一个Timer构造,它允许我触发一个可重置的一次性定时器并关联并给它一个回调,它可以对创建定时器的实体执行一些操作,在这种情况下是一个节点树。

在查看各种选项后,我找不到任何我可以原生使用的东西(比如一些django应用程序)。 Python中的Timer对象不适用于此,因为它不会扩展/执行。因此,我决定根据以下内容编写自己的计时器:

  1. 包含时间范围的时间 - 增量对象的排序列表
  2. 触发“tick”的机制
  3. 实施选择:

    1. 在Bisect周围找到已排序的delta列表的包装器: http://code.activestate.com/recipes/577197-sortedcollection/
    2. 与芹菜一起提供勾选 - 粒度为1分钟,工作人员将触发我的Timer类提供的timer_tick函数。 timer_tick本质上应该通过排序列表,每次滴答都减少头节点。然后,如果任何节点已经下降到0,则启动回调并从排序的计时器列表中删除这些节点。
    3. 创建计时器涉及实例化Timer对象,该对象返回对象的id。此id存储在db中,并与DB中的条目相关联,该条目表示创建计时器的实体
    4. 其他数据结构:  为了跟踪Timer实例(为每个定时器创建实例化)我有一个WeakRef字典,它将id映射到obj

      基本上,我在主Django项目的内存中有2个数据结构。

      问题陈述:

      由于芹菜工人需要走掉计时器列表并且还可能修改id2obj地图,看起来我需要找到一种方法来分享我的芹菜工人和主菜之间的状态

      通过SO / Google,我发现了以下建议

      1. 管理器
      2. 共享内存
      3. 不幸的是,bisect包装器不适合酸洗和/或状态共享。我通过创建一个dict并尝试在Dict中嵌入已排序的List来尝试管理器方法。它出现了一个错误(由于排序列表保存的内存未被共享并将其嵌入“共享“内存对象不起作用”

        ...最后问题:

        1. 有没有办法可以与工作线程分享我的SortedCollection和Weakref Dict
        2. 替代解决方案:

          如何保持工作线程简单...让它为DB写入每个tick,然后使用post Db信号在main上获得通知并执行main中过期定时器的处理。当然,问题在于我失去了并行化。

1 个答案:

答案 0 :(得分:0)

让我们从现有实施的一些评论开始:

  

在Bisect周围找到已排序的delta列表的包装器:http://code.activestate.com/recipes/577197-sortedcollection/

虽然这会给你O(1)弹出(只要你按相反的时间顺序保持列表),它会使每个插入O(N)(对于不太常见的操作,例如删除任意作业,如果你有“取消“API”。由于你所做的插入与弹出一样多,这意味着整个过程在算法上并不比未排序的列表好。

heapq替换它(这正是它们的用途)给你O(log N)插入。 (请注意,Python的heapq没有peek,但这是因为heap[0]等同于heap.peek(0),因此您不需要它。)

如果你需要进行其他操作(取消,非破坏性地迭代等)O(log N),你需要一个搜索树;查看PyPI上的blistbintrees以获取一些好的。


  

用芹菜提供滴答 - 粒度为1分钟,工人将触发我的Timer类提供的timer_tick函数。 timer_tick本质上应该通过排序列表,每次滴答都减少头节点。然后,如果任何节点已经下降到0,则启动回调并从排序的计时器列表中删除这些节点。

保持目标时间而不是增量更好。有了目标时间,你只需要这样做:

while q.peek().timestamp <= now():
    process(q.pop())

同样,这是O(1)而不是O(N),并且它更简单,并且它将队列中的元素视为不可变的,并且它避免了任何可能的问题,迭代花费的时间超过了您的滴答时间(可能1分钟刻度不是问题......)。


现在,回答你的主要问题:

  

有没有办法可以分享我的SortedCollection

是。如果您只想要(timestamp, id)对的优先级堆,您可以将其放入multiprocessing.Array,就像list一样容易,除了需要明确跟踪长度。然后你只需要同步每个操作,并且......就是这样。

如果你只是一次/分钟滴答,并且你希望经常忙碌,你可以使用Lock进行同步,并让计划工作者自己勾选。 / p>

但老实说,我完全放弃了滴答,只使用Condition - 它更灵活,概念上更简单(即使它的代码更多),这意味着你使用的是0%CPU当你没有工作要做,并在你负载时快速顺利地做出反应。例如:

def schedule_job(timestamp, job):
    job_id = add_job_to_shared_dict(job) # see below
    with scheduler_condition:
        scheduler_heap.push((timestamp, job))
        scheduler_condition.notify_all()

def scheduler_worker_run_once():
    with scheduler_condition:
        while True:
            top = scheduler_heap.peek()
            if top is not None:
                delay = top[0] - now()
                if delay <= 0:
                    break
                scheduler_condition.wait(delay)
            else:
                scheduler_condition.wait()
        top = scheduler_heap.pop()
        if top is not None:
            job = pop_job_from_shared_dict(top[1])
            process_job(job)

无论如何,这将我们带到了充满工作的弱势群体。

由于weakdict明确存储对进程内对象的引用,因此跨进程共享它是没有任何意义的。您想要存储的是不可变对象,它们定义了作业的实际内容,而不是可变作业本身。然后它只是一个普通的旧词典。

但是,一个普通的旧词典在各个流程中分享并不是一件容易的事。

这样做的简单方法是使用dbm数据库(或围绕一个shelve包装器)而不是内存dict,与{{1}同步}。但这意味着每次有人想要更改数据库时都要重新刷新并重新打开数据库,这可能是不可接受的。

切换到一个sqlite3数据库可能看起来有些过分,但它可能会简单得多。

另一方面......你实际拥有的唯一操作是“将下一个id映射到此作业并返回id”和“pop并返回此id指定的作业”。这真的需要成为一个词典吗?键是整数,您可以控制它们。一个Lock,加上一个Array代表下一个密钥,一个Value,你差不多完成了。问题是你需要某种密钥溢出方案。而不仅仅是Lock,您必须翻转,并检查已使用的插槽:

next_id += 1

另一种选择是将dict存储在主进程中,并使用with lock: next_id += 1 if next_id == size: next_id = 0 if arr[next_id] is None: arr[next_id] = job return next_id 让其他进程查询它。