据说是复杂度O(1)的牛逼算法,所以抽时间学习学习。
现在要实现一个定时器,这个定时器控制很多任务。该怎么做呢?
第一反应是任务做成一个队列,属性有个时间,每次计时后将该属性减1,到0的时候就执行。这种方式可行,但是效率不高,因为每次都要遍历所有任务,所以时间复杂度是O(N)。
优化的方法是什么呢?有点类似哈希表,增加一个时间队列,同时将任务预先排放在一个时间队列中。如果是100秒的时间范围,那么就是100个队列,10秒延时的任务就放在第9个(起始是0),20秒延时的任务就放在第19个,这里的位置就叫做槽slot。这样每次计数的时候就不用去遍历任务队列,只用去看当前时间队列有没有任务就行了。
如果时间很长呢?这个队列占用的内存岂不是无止境,所以改成循环的方式,比如60秒是一个循环,那么100秒就是100/60=1,此时余数是40,所以这个任务就放在slot40,此时圈数circle就是1。之后在时间间隔中挨着减就可以了。虽然增加了一个时间队列,但是这个耗费不算多,但是算法的时间复杂度就变成了O(1)。
算法示意如下:
python代码:
import threading
import time
from collections import defaultdict
class Task:
def __init__(self, delay, callback, args=()):
self.delay = delay # 单位秒
self.callback = callback
self.args = args
self.circle = 0 # 圈数
self.slot = 0 # 槽位
class TimingWheel:
def __init__(self, interval=1, slot_num=60):
self.interval = interval # 每个tick的时间间隔(秒)
self.slot_num = slot_num # 槽数量(环大小)
self.slots = defaultdict(list)
self.current_slot = 0
self.lock = threading.Lock()
self.running = False
self.thread = None
def add_task(self, task):
with self.lock:
ticks = int(task.delay / self.interval)
task.slot = (self.current_slot + ticks) % self.slot_num
task.circle = ticks // self.slot_num
self.slots[task.slot].append(task)
def tick(self):
with self.lock:
slot_tasks = self.slots[self.current_slot]
new_tasks = []
for task in slot_tasks:
if task.circle > 0:
task.circle -= 1
new_tasks.append(task)
else:
threading.Thread(target=task.callback, args=task.args).start()
self.slots[self.current_slot] = new_tasks
self.current_slot = (self.current_slot + 1) % self.slot_num
def start(self):
self.running = True
def run():
while self.running:
time.sleep(self.interval)
self.tick()
self.thread = threading.Thread(target=run)
self.thread.start()
def stop(self):
self.running = False
if self.thread:
self.thread.join()
# 示例用法
def my_task(name):
print(f"[{time.strftime('%X')}] Task executed: {name}")
if __name__ == "__main__":
wheel = TimingWheel(interval=1, slot_num=10)
wheel.start()
# 添加几个任务
wheel.add_task(Task(3, my_task, args=("Task A",)))
wheel.add_task(Task(7, my_task, args=("Task B",)))
wheel.add_task(Task(12, my_task, args=("Task C",)))
wheel.add_task(Task(25, my_task, args=("Task D",)))
time.sleep(30) # 等待任务完成
wheel.stop()
最后一个小问题,槽数怎么定?
一般公式是:
槽数 = 最大期望延迟 / 时间粒度
如果想支持最长 10 秒的延迟,精度是 100ms,则:
槽数 = 10s / 0.1s = 100
一些典型的槽数:
使用场景 | 推荐粒度 | 推荐槽数范围 |
---|---|---|
网络心跳检测 | 1s | 60~600 |
游戏逻辑更新 | 100ms | 100~1000 |
高频延时任务 | 1ms | 1000+ |
大跨度任务(分钟级) | 1s 或 1min | 分层时间轮更合适 |
好了,先到这里吧。