【问题标题】:Deterministic assignment of tasks to threads using ExecutorService使用 ExecutorService 将任务确定性地分配给线程
【发布时间】:2014-07-31 09:31:10
【问题描述】:

给定具有固定线程池的 Executor 服务,是否可以保证将任务确定性地分配给线程?更准确地说,假设只有两个线程,即 pool-thread-0 和 pool-thread-1,并且有 2 个要执行的任务的集合。我希望实现的是前一个线程总是执行第一个,而后者处理剩下的。

这是一个例子:

public static void main(String[] args) throws InterruptedException, ExecutionException {
    ExecutorService executorService = newFixedThreadPool(2,
            new ThreadFactoryBuilder().setNameFormat("pool-thread-%d").build());

    for (int i = 0; i < 5; i++) {
        List<Callable<Integer>> callables = ImmutableList.of(createCallable(1), createCallable(2));
        executorService.invokeAll(callables);

    }
}

public static Callable<Integer> createCallable(final int task) {
    return new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            currentThread().sleep(1000);
            System.out.println(Thread.currentThread().getName() + " executes task num: " + task);
            return task;
        }
    };
}

我机器的示例输出:

pool-thread-0 executes task num: 1
pool-thread-1 executes task num: 2

pool-thread-0 executes task num: 2
pool-thread-1 executes task num: 1

pool-thread-0 executes task num: 2
pool-thread-1 executes task num: 1

pool-thread-0 executes task num: 2
pool-thread-1 executes task num: 1

pool-thread-0 executes task num: 1
pool-thread-1 executes task num: 2

简而言之,我希望确保 pool-thread-0 始终执行第一个任务。任何帮助将不胜感激!

【问题讨论】:

  • 我不这么认为。如果您需要这种确定性,为什么不设置两个队列和两个池?
  • 即使可以,您为什么想要或关心?即使您可以保证,如果没有某种形式的同步,它也不会对执行顺序产生任何可重复的影响。
  • @Baldy 我想避免过多介绍细节,但是每个线程都有一个随机数据生成器,我需要确保该过程是可重复的。假设我需要提供它们处理的线程和数据的组合是可重复的。
  • @Kylar 你的想法确实没有那么不好,但是它不能很好地概括。
  • @voo 你需要你的第一个任务在你的第一个线程上运行,而不是别的,还是你还需要第 2 个任务来在一个特定的线程上执行,等等?

标签: java multithreading concurrency executorservice


【解决方案1】:

ExecutorService 并非旨在为其 Callable/Runnable 提供“线程关联”。有人可能会争辩说“这就是重点”,API 可以让程序员处理工作描述(Callable),而不是线程处理。

您的设计,因为“每个线程都有一个随机数据生成器”不适合 ExecutorService,我看到三个原因:

  1. 您无法控制将创建(或销毁!)哪些线程以及何时创建(如果一个线程崩溃了怎么办?池将重新创建它,但它会得到什么随机生成器?)。所以我们不能推断出“这个线程”有“这个生成器”的可靠方式,更不用说“第二个”线程有“这个生成器”,因为甚至可能没有第二个线程(如果每个任务都这么快他们的处理速度比您派遣他们的速度更快?)。

  2. 您无法控制何时执行哪些任务。好吧...使用 Executors.newFixedThreadPool,您可以按照提交的顺序分派它们,但就您所知,操作系统调度程序可能会将所有优先级赋予线程 1,这将最终完成所有工作,并且线程 2 将什么也不做(它可以是介于两者之间的任何比例)。

  3. 将“数据生成器”传递给线程的唯一方法是覆盖执行程序服务的 ThreadFactory。否则,您将无权访问线程实例(在运行时从可调用自身中分离出来)。因此,要将特定生成器与特定线程相关联,您必须知道当前正在创建的线程编号,如果您正在计算线程,这很容易,但如果您想知道这个 Callable 是什么则很困难线程旨在(参见第 2 点)。

所以我强烈建议您定义一些其他方式将您的工作单元与您的数据生成器相关联,因为“线程实例”通常是不可靠的 - 至少不是通过执行器服务。 例如。当你说

我需要提供它们处理的线程和数据的组合是可重复的。

我了解您将始终调度一定数量的 Callable,并且您需要它们中的每一个处理由特定生成器发出的特定数据集。假设我们有给定数量的任务和 3 个生成器,task(N) 将使用生成器 N%3

为了使结果可重复,您还需要使用同一生成器的任务不并发执行(您希望通过线程关联来实现什么?)。

有一定数量的模式可以实现这一点。

1 是:重构为生产者/消费者(反过来做)

在您的执行器服务中创建 3 个任务,每个任务都听一个 BlockingQueue(它的私有等待列表)并拥有自己的私有生成器。这些是消费者。
使您的主线程成为生产者:当它创建编号为 N 的工作单元(在您的原始设计中曾经是 Callable)时,将其分派到编号为 N%3 的等待队列中。就是这样:每个消费者将按您希望的顺序按顺序接收自己的要计算的数据。你已经达到了“亲和力”。

2 是:让任务调度任务自己。 (用老套的方式做)

首先,重构您的可调用对象以链接到他们需要使用的生成器。 然后,在您的主线程上,构建要为每个生成器运行的任务列表。
从主线程分派每个生成器的第一个任务。
并且在每个可调用对象的末尾,使可调用对象调度其列表中的下一个工作单元。
注意不要“把你锁在外面”,但是,如果你从可调用对象中分派可调用对象,不要等待结果,因为这会阻止可调用对象完成,从而阻止新分派的对象执行。这是一个死锁。

3 是 : 与 2 相同,效率较低,但风险较小

不要从可调用对象内部分派可调用对象,而是仅从您的主线程分派,通过等待期货。

使用这两种方式中的任何一种,您都无法保证哪些任务将首先完成或最后完成,但您可以保证您调度的工作单元可预测地与您控制的数据生成器相关联,并且它们将在命令你派遣他们。希望这就足够了。

【讨论】:

  • 谢谢,在阅读了您的建议后,我成功地重新设计了我的代码。
猜你喜欢
  • 2012-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多