【问题标题】:Java- Efficient Scheduling Structure?Java-高效的调度结构?
【发布时间】:2011-11-13 20:15:05
【问题描述】:

我为这个问题的长度道歉,但我认为包含足够的细节很重要,因为我正在寻找一种合适的方法来解决我的问题,而不是简单的代码建议!


一般说明:

我正在开展一个项目,该项目需要能够以某个相对重复间隔“安排”任务。

这些间隔是根据一些内部时间来表示的,它表示为一个随着程序执行而递增的整数(因此不等于实时)。每次发生这种情况时,都会询问计划以检查是否有任何要在此时间步执行的任务。

如果一个任务被执行,它应该被重新安排在相对于当前时间的位置再次运行(例如,以 5 个时间步长)。这个相对位置被简单地存储为 Task 对象的一个​​整数属性。

问题:

我有点难以决定我应该如何构建这个 - 部分原因是它是一组稍微难以查找的搜索词。

就目前而言,我认为每次定时器增加时我都需要:

  1. 在计划中的“0”位置执行任务
  2. 将这些任务重新添加到计划中的相对位置(例如,每 5 步重复的任务将返回到位置 5)
  3. 计划中的每组任务的“执行前时间”将减一(例如,位置 1 的任务将移至位置 0)

假设:

有几个假设可能会限制我可以使用的可能解决方案:

  • 间隔必须是相对的,而不是特定的时间,并且定义为从当前时间开始的整数步数
  • 这些间隔可以采用任何整数值,例如没有界限
  • 多个任务可能安排在同一个时间步,但它们的执行顺序并不重要
  • 所有执行都应保持在单线程中 - 由于其他限制,多线程解决方案不适合

我的主要问题是:

我怎样才能设计这个时间表以有效地工作?哪些数据类型/集合可能有用?

我应该考虑另一种结构/方法吗?

我是否错误地忽略了调度框架(例如 Quartz),它似乎在“实时”时域而不是“非实时”时域中工作得更多?


非常感谢任何可能的帮助。如有需要,请随时评论以获取更多信息,我会在需要的地方进行编辑!

【问题讨论】:

  • 在我看来,您想通过计数器在应用程序中模拟时间。我想知道为什么您不想使用系统时间,即实时时间?
  • 当然你可以用石英做到这一点,你以前用过石英吗。
  • @user384706 - 我在模拟情况下工作,每个时间步长可能需要非常不同的时间长度,因此表示为一个计数器。
  • @Java Ja Baby - 我以前没有使用过 Quartz,但据我所知,它更专注于“实时”调度(正如我在 OP 中所说)。这种信念是错误的吗?
  • 我知道你有一个特定的问题,不想成为一个“痛苦”,但我想知道,你不能简单地在一个单独的类中对需要不同时间长度的每个时间步进行建模并安排它使用实时根据特定的班级间隔进行调度?

标签: java algorithm scheduling


【解决方案1】:

这个怎么样,它使用你自己的 Ticks 和 executeNextInterval() :

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class Scheduler {
    private LinkedList<Interval> intervals = new LinkedList<Scheduler.Interval>();

    public void addTask(Runnable task, int position) {
        if(position<0){
            throw new IllegalArgumentException();
        }
        while(intervals.size() <= position){
            intervals.add(new Interval());
        }
        Interval interval = intervals.get(position);
        interval.add(task);
    }

    public void executeNextInterval(){
        Interval current = intervals.removeFirst();
        current.run();
    }
    private static class Interval {
        private List<Runnable> tasks = new ArrayList<Runnable>();
        public void add(Runnable task) {
            tasks.add(task);
        }
        public void run() {
            for (Runnable task : tasks) {
                task.run();
            }
        }
    }
}

您可能想要添加一些错误处理,但它应该可以完成您的工作。

这里有一些 UnitTests :)

import junit.framework.Assert;

import org.junit.Test;

public class TestScheduler {
    private static class Task implements Runnable {
        public boolean didRun = false;
        public void run() {
            didRun = true;
        }       
    }
    Runnable fail = new Runnable() {
        @Override
        public void run() {
            Assert.fail();
        }
    };

    @Test
    public void queue() {
        Scheduler scheduler = new Scheduler();
        Task task = new Task();
        scheduler.addTask(task, 0);
        scheduler.addTask(fail, 1);
        Assert.assertFalse(task.didRun);
        scheduler.executeNextInterval();
        Assert.assertTrue(task.didRun);
    }
    @Test
    public void queueWithGaps() {
        Scheduler scheduler = new Scheduler();
        scheduler.addTask(fail, 1);
        scheduler.executeNextInterval();
    }
    @Test
    public void queueLonger() {
        Scheduler scheduler = new Scheduler();
        Task task0 = new Task();
        scheduler.addTask(task0, 1);
        Task task1 = new Task();
        scheduler.addTask(task1, 1);
        scheduler.addTask(fail, 2);
        scheduler.executeNextInterval();
        scheduler.executeNextInterval();
        Assert.assertTrue(task0.didRun);
        Assert.assertTrue(task1.didRun);
    }
}

【讨论】:

  • 哎呀!为什么要重新发明轮子?请参阅ScheduledExecutorService(以及那里的示例)
  • 但那是基于时间和定时延迟,而不是用户滴答声。如果您不知道每个刻度的长度以及每个刻度的长度是否不同,这将无济于事。
  • 正如@flob 正确指出的那样,我不是在实时域中工作。系统的每个滴答声/步骤可能在实时持续时间内变化很大,因此我不相信不能以这种方式表示。如果我正在处理实时域类型问题,我会始终坚持使用标准 API,而不是像你说的那样“重新发明轮子”。
  • 谢谢,也感谢随附的测试!尽管我以后可能需要在某些方面进行优化,但按照与此相同的想法开发一些东西应该符合我的目的。
【解决方案2】:

如果我必须这样做,我会创建一个简单的队列(链表变体)。该队列将包含一个动态数据结构(例如简单列表),其中包含所有需要完成的任务。在每个时间间隔(或时间步长),进程读取队列的第一个节点,执行它在该节点列表中找到的指令。在每次执行结束时,它将计算重新调度并将新的执行添加到队列中的另一个节点,或者在将指令存储在该节点中之前创建直到该位置的节点。然后删除第一个节点,并在下一个时间步执行第二个节点(现在是第一个节点)。该系统也不需要跟踪任何整数,并且所需的所有数据结构都可以在 java 语言中找到。这应该可以解决您的问题。

【讨论】:

  • 这听起来很像我打算自己实现。我对这种方法唯一关心的是效率,但也许这是可怕的“过早”优化问题。
  • 先构建你的程序,如果性能很麻烦,再优化。不要优化你还没有做的东西。此解决方案的效率应该足以满足您的需求。
【解决方案3】:

看看DelayQueue 使用PriorityQueue 来维护这样一个有序的事件列表的方式。 DelayQueue 使用实时工作,因此可以使用ConditionLockSupport 中可用的可变定时等待方法。您可以实现类似SyntheticDelayQueue 的东西,其行为方式与DelayQueue 相同,但使用您自己的合成时间服务。不过,您显然必须用 jdk 替换免费提供的定时等待/信号机制,这对于高效地进行可能并非易事。

【讨论】:

    【解决方案4】:

    这里有几个想法:

    保持一切简单。如果您没有数百万个任务,则不需要优化的数据结构(骄傲或过早优化的冲动除外)。

    避免相对时间。使用绝对内部刻度。如果添加任务,请将“下次运行”设置为当前刻度值。将其添加到列表中,按时间对列表进行排序。

    查找任务时,从列表的开头开始,选择所有有时间

    在另一个列表中收集所有这些任务。全部运行后,根据当前刻度和增量计算“下一次运行”(这样您就不会得到循环的任务),将它们全部添加到列表中,排序。

    【讨论】:

      【解决方案5】:

      使用ScheduledExecutorService。它内置了您需要的一切。使用起来非常简单:

      // Create a single-threaded ScheduledExecutorService
      ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); // 1 thread
      
      // Schedule something to run in 10 seconds time 
      scheduler.schedule(new Runnable() {
          public void run() {
              // Do something
          }}, 10, TimeUnit.SECONDS);
      
      // Schedule something to run in 2 hours time 
      scheduler.schedule(new Runnable() {
          public void run() {
              // Do something else
          }}, 2, TimeUnit.HOURS);
      
      // etc as you need
      

      【讨论】:

      • 感谢您的回答。但是,引用 OP-“这些间隔是根据一些内部时间表示的,它表示为一个整数,随着程序执行而增加(因此不等于实时)”。我相信这意味着 ScheduledExecutorService 对我没有用,对吗?
      • 不正确:最终,“内部表示”必须转换为实时,否则任务将永远不会被调度为实际 运行(在实时时间)。这个答案可以使用任意时间间隔——这就是我通过选择秒或小时作为间隔来演示的。可悲的是,“已接受”的答案是如此糟糕:它没有安排任何事情,它只是毫无延迟地连续执行它们。当 JDK 中存在完美的轮子时,您应该永远重新发明轮子!
      • 也许使用术语“调度”是一个错误,但实际上接受的答案正是我所需要的。只要对获取下一个“间隔”任务的调用进行了适当的管理(也就是每个“内部”时间滴答只调用一次),它就可以工作,这在我的情况下是完全可行的。在您的解决方案中,您建议我在“3 个模拟时间步长”中使用 TimeUnit 的什么值?当这些持续时间不一致时,我根本不认为这是一个可行的答案?
      【解决方案6】:

      嗯,Quartz 是非常强大的工具,但是它的配置可能性有限,所以如果你需要特定的功能,你应该适当地编写自己的解决方案。

      但是,研究 Quartz 源代码和数据结构是个好主意,因为它们已经成功地解决了很多问题,你会发现 f.g.数据库级别的进程间同步,运行延迟任务等。

      我曾经编写过我自己的调度程序,它适用于可能 Quartz 不容易适应的任务,但是一旦我学会了 Quartz,我就明白我可以在我的解决方案中改进多少,知道它是如何改进的是在 Quartz 中完成的。

      【讨论】:

      • 感谢您的建议 :)。老实说,在询问了 SO 之后,检查开源调度程序的内部结构是我的下一个停靠点。我认为最好先在这里问的唯一原因是因为“非实时域”部分 - 我认为这是一个比人们想要“每 5 分钟安排一次”更不常见的问题,因此可能有其他选择我应该考虑的方法!
      【解决方案7】:

      循环链表可能是您正在寻找的数据结构。您只需增加循环任务列表中“当前”字段的索引,而不是减少每个任务元素中的字段。伪代码结构可能如下所示:

      tick():
          current = current.next()
          for task : current.tasklist():
              task.execute()
      

      任何时候你安排一个新任务,你只需将它添加到当前'tick'前N个刻度的位置

      【讨论】:

      • 感谢您的建议,但是这是否适用于以下情况:1) 并非所有时间步都会关联一个任务,以及 2) 最大调度间隔在执行期间可能会有所不同?我知道我之前没有解释过这些假设!
      • 只要时间步长总是基值的整数倍,你应该没问题。对于无任务步骤,您只需在列表中有一个空条目,而对于更改执行时间,您可以在列表中添加/删除条目。它在空间方面并不优雅或高效,但它可以巧妙地解决维护柜台的问题。
      猜你喜欢
      • 2023-03-19
      • 1970-01-01
      • 1970-01-01
      • 2023-03-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多