【问题标题】:Implementing a dynamic multiple timeline queue实现动态多时间线队列
【发布时间】:2012-06-24 14:58:55
【问题描述】:

简介

我想实现一个动态多时间线队列。这里的上下文一般是调度

什么是时间线队列

这仍然很简单:它是一个任务时间线,其中每个事件都有其开始和结束时间。任务被分组为作业。这组任务需要保持其顺序,但可以作为一个整体及时移动。例如它可以表示为:

 --t1--   ---t2.1-----------t2.2-------
 '    '   '        '                  '
20    30  40       70                120 

我会将其实现为带有一些额外约束的heap queue。 Python sched 模块在这个方向上有一些基本方法。

定义多时间线队列

一个队列代表一个资源,一个任务需要一个资源。图形示例:

R1  --t1.1----- --t2.2-----      -----t1.3--    
            /  \                /
R2  --t2.1--     ------t1.2-----


解释“动态

当一项任务可以使用多种资源中的一种时,它变得很有趣。另一个限制是,可以在同一资源上运行的连续任务必须使用同一资源。

示例:如果(从上面)任务t1.3 可以在R1R2 上运行,则队列应如下所示:

R1  --t1.1----- --t2.2-----      
            /  \                
R2  --t2.1--     ------t1.2----------t1.3--    


功能(按优先顺序)

  • FirstFreeSlot(duration, start):找到从start开始的第一个空闲时间段,duration有空闲时间(详见文末说明)。
  • 通过考虑约束条件(主要是:任务的正确顺序、同一资源上的连续任务)并使用FirstFreeSlot,尽可能早地将作业入队
  • 在特定时间放置工作并向后移动尾巴
  • 删除工作
  • 重新计算:删除后,测试一些任务是否可以提前执行。


关键问题

重点是:我如何表示这些信息以有效地提供功能?实施取决于我;-)

更新:需要考虑的另一点:典型的区间结构关注“X 点是什么?”但在这种情况下,enqueue 以及因此的问题“持续时间 D 的第一个空槽在哪里?”重要得多。所以这个方向的段/区间树或其他东西可能不是正确的选择。

进一步详细说明空闲时隙:由于我们有多个资源和分组任务的限制,某些资源上可以有空闲时隙。简单示例:t1.1 在 R1 上运行 40,然后t1.2 在 R2 上运行。所以R2上有一个空的区间[0, 40],可以被下一个job填满。


更新 2:有一个 interesting proposal in another SO question。如果有人可以将它移植到我的问题并表明它适用于这种情况(特别是针对多种资源进行详细阐述),那么这可能是一个有效的答案。

【问题讨论】:

  • 您能举例说明其中一项任务可能是什么吗?
  • @327 No :) 我将分析/评估调度算法。因此,任务只是一个抽象对象,具有持续时间和一组可能运行的资源。
  • 我还有一些疑问: 1.什么是工作:t1.1 和 t1.2 是工作? 2.为什么t1.3可以同时在R1和R2上运行?这是否意味着 t1.2 可以在 R1 和 R2 上运行?
  • @zinking 1. t1.x 是一组任务。 2. 这只是一个例子,这是动态的。 3. 不可以。如果t1.2 可以在R1 上运行,由于组限制,它将被安排在那里。
  • 你无法在不知道如何使用数据结构的情况下单独评估它。这也使您对算法产生疑问。选择一个调度算法,并根据一个简单的数据结构评估它的时间复杂度。复杂度是由算法的逻辑还是数据结构决定的?只有数据结构限制了复杂性,才值得改进。

标签: python performance algorithm data-structures intervals


【解决方案1】:

让我们首先将自己限制在最简单的情况下:找到一个合适的数据结构,以便快速实现 FirstFreeSlot()

空闲时隙存在于二维空间中:一维是开始时间 s,另一个是长度 d。 FirstFreeSlot(D) 有效地回答了以下查询:

min s: d >= D

如果我们将 s 和 d 视为笛卡尔空间 (d=x, s=y),这意味着在以垂直线为界的子平面中找到最低点。 quad-tree,可能在每个节点中都有一些辅助信息(即所有叶子的 min s),将有助于有效地回答这个查询。

对于 Enqueue() 面对资源限制,考虑为每个资源维护一个单独的四叉树。四叉树还可以回答诸如

之类的查询

min s: s >= S & d >= D

(限制起始数据所必需的)以类似的方式:现在一个矩形(在左上角打开)被切断,我们在那个矩形中寻找 min s。

Put()Delete() 是四叉树的简单更新操作。

Recalculate() 可以通过Delete() + Put() 来实现。为了节省不必要的操作时间,定义充分(或者,理想情况下,充分+必要)条件来触发重新计算。 Observer 模式在这里可能会有所帮助,但请记住将重新调度的任务放入 FIFO 队列或按开始时间排序的优先级队列。 (您希望在接手下一个任务之前完成重新安排当前任务。)

总的来说,我相信您知道大多数类型的调度问题,尤其是那些有资源限制的问题,至少是 NP 完全的。因此,在一般情况下,不要指望具有良好运行时间的算法。

【讨论】:

  • 因为d 是来自当前项目的s 和来自上一个项目的s 的计算属性,我认为这不是最佳解决方案。但是另外存储空闲插槽绝对是要走的路,所以+1。
  • 为什么它必须是计算属性?我的想法是四叉树存储调度的当前状态,并且每次更改都会导致四叉树的更新。
  • 是的,但是更新树的成本很高,所以可能有更好的解决方案。
  • 确实如此。 O(log N) 比 O(1) 更昂贵。 ;-)
  • 好吧,我认为更新不是那么简单。我认为更新单个节点是最有效的方法,通过删除并重新插入新数据。删除的平均情况的复杂度为O(n log n)。但是put 需要更新node.s >= new_node.s 所在的每个节点。在这种情况下,“命中”最坏删除情况 (O(n²)) 的概率非常高。所以我们至少有 O(n² log n) 甚至 O(n³) 用于 put 操作。
【解决方案2】:
class Task:
    name=''
    duration=0
    resources=list()

class Job:
    name=''
    tasks=list()

class Assignment:
    task=None
    resource=None
    time=None

class MultipleTimeline:
    assignments=list()
    def enqueue(self,job):
        pass
    def put(self,job):
        pass
    def delete(self,job):
        pass
    def recalculate(self):
        pass

这是朝着您正在寻找的方向迈出的第一步,即用 Python 编写的数据模型吗?

更新:

这是我更高效的模型:

它基本上把所有的任务都放在一个按结束时间排序的链表中。

class Task:
    name=''
    duration=0    # the amount of work to be done
    resources=0   # bitmap that tells what resources this task uses
# the following variables are only used when the task is scheduled
    next=None     # the next scheduled task by endtime
    resource=None # the resource this task is scheduled
    gap=None      # the amount of time before the next scheduled task starts on this resource

class Job:
    id=0
    tasks=list() # the Task instances of this job in order 

class Resource:
    bitflag=0       # a bit flag which operates bitwisely with Task.resources
    firsttask=None  # the first Task instance that is scheduled on this resource
    gap=None        # the amount of time before the first Task starts

class MultipleTimeline:
    resources=list()
    def FirstFreeSlot():
            pass
    def enqueue(self,job):
        pass
    def put(self,job):
        pass
    def delete(self,job):
        pass
    def recalculate(self):
        pass

由于enqueueput 的更新,我决定不使用树。 因为put 会及时移动任务,所以我决定不使用绝对时间。

FirstFreeSlot 不仅返回具有空闲槽的任务,还返回其他正在运行的任务及其结束时间。

enqueue 的工作原理如下: 我们通过FirstFreeSlot 寻找一个空闲位置并在这里安排任务。 如果下一个任务有足够的空间,我们也可以安排它。 如果没有:查看正在运行的其他任务是否有可用空间。 如果不是:运行FirstFreeSlot,并带有本次参数和正在运行的任务。

改进: 如果put 不经常使用并且enqueue 从零开始完成,我们可以通过在每个包含其他正在运行的任务的任务中包含一个 dict() 来跟踪重叠的任务。然后,也很容易为每个资源保留一个 list(),其中包含按 endtime 排序的该资源的绝对时间的计划任务。仅包括那些比以前具有更大时间间隔的任务。现在我们可以更轻松地找到空位。

问题: put安排的任务需要在那个时候执行吗? 如果是:如果 put 安排的另一个任务重叠怎么办? 是否所有资源都以同样快的速度执行任务?

【讨论】:

    【解决方案3】:

    经过一段时间的思考。我认为段树可能更适合为这个时间线队列建模。作业概念类似于 LIST 数据结构。

    我假设任务可以像这样建模(伪代码)。作业中的任务顺序可以通过start_time来保证。

    class Task:
        name=''
    
        _seg_starttime=-1; 
        #this is the earliest time the Task can start in the segment tree, 
        #a lot cases this can be set to -1, which indicates its start after its predecessor,
        #this is determined by its predecessor in the segment tree.
        #if this is not equal -1, then means this task is specified to start at that time
        #whenever the predecessor changed this info need to be taken care of
    
        _job_starttime=0; 
        #this is the earliest time the Task can start in the job sequence, constrained by job definition
    
        _duration=0;      
        #this is the time the Task cost to run
    
        def get_segstarttime():
           if _seg_starttime == -1 :
               return PREDESSOR_NODE.get_segstarttime() + _duration
           return __seg_startime + _duration
    
        def get_jobstarttime():
           return PREVIOUS_JOB.get_endtime()
    
        def get_starttime():
           return max( get_segstarttime(), get_jobstarttime() )
    
    • 入队它只是将一个任务节点附加到段树中,注意 _seg_startime 设置为 -1 表示它在它的前任之后立即启动
    • Put 在树中插入一个段,段由 start_time 和 duration 表示。
    • 删除删除树中的段,必要时更新其后继节点(例如,删除的节点是否存在 _seg_start_time)
    • 重新计算 再次调用 get_starttime() 将直接得到它的最早开始时间。

    示例(不考虑工作限制)

                      t1.1( _segst = 10, du = 10 )
                          \
                          t2.2( _segst = -1, du = 10 ) meaning the st=10+10=20
                            \
                            t1.3 (_segst = -1, du = 10 ) meaning the st = 20+10 = 30
    

    如果我们执行 Put:

                        t1.1( _segst = 10, du = 10 )
                            \
                            t2.2( _segst = -1, du = 10 ) meaning the st=20+10=30
                            /  \
    t2.3(_segst = 20, du = 10)  t1.3 (_segst = -1, du = 10 ) meaning the st = 30+10 = 30
    

    如果我们对原始场景执行删除 t1.1

                          t2.2( _segst = 20, du = 10 ) 
                            \
                            t1.3 (_segst = -1, du = 10 ) meaning the st = 20+10 = 30
    

    每个资源都可以用这个区间树的 1 个实例来表示 鸡蛋。

    从线段树(时间线)的角度来看:

    t1.1                      t3.1
      \                        / \
      t2.2                  t2.1  t1.2
    

    从工作的角度来看:

    t1.1 <- t1.2
    t2.1 <- t2.2
    t3.1
    

    t2.1 和 t2.2 使用链表连接,如下所述:t2.2 从段树中获取它的 _sg_start_time,从链表中获取它的 _job_start_time,比较这两个时间,然后是它可能的实际最早时间可以导出run。

    【讨论】:

    • +1 获取详细说明。但是段树绝对不是正确的选择:“一旦结构建立,它的内容就不能被修改”。而且我不认为间隔树是这种情况下的最佳解决方案,因为我在单个资源上没有重叠间隔,并且资源数量与任务数量相比很小。我认为正确的方向是每个资源都链接在一起的一些二进制结构。
    • @ms4py 我同意你所说的,一旦结构建成,它的内容就不能被修改。但真正重要的只是任务的 start_time,这就是为什么我从各种内部结构构造 start_time。这基本上使我们不需要处理 start_time。也许我没有让自己很好理解,我在想这个实现可以处理你提到的功能。
    • 我更新了这个问题,增加了一点需要考虑。而且我没有在您的模型中看到多个资源的表示。
    • @ms4py 我稍微更新了我的解释,不同树实例之间的节点通过链表在作业中连接。我可能没有很好地解释这一点。
    • 另外,我可能不应该强调这是段树,因为你说没有太多重叠。但是当我们做一个put的时候,就会发生重叠,然后就可以利用这个segment tree insert的概念了。
    【解决方案4】:

    我最终只使用了一个简单的列表来存储我的队列项,并使用一个内存中的 SQLite 数据库来存储空槽,因为使用 SQL 进行多维查询和更新非常有效。我只需要将字段 startdurationindex 存储在一个表中。

    【讨论】:

    • :-) 只需使用一个关系,然后让优化器找到最佳路径。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-28
    • 1970-01-01
    • 1970-01-01
    • 2018-11-14
    • 1970-01-01
    • 2018-06-30
    相关资源
    最近更新 更多