【问题标题】:Using Java multithreading, what is the most efficient to coordinate finding the best result?使用 Java 多线程,什么是最有效的协调找到最佳结果?
【发布时间】:2015-05-15 03:07:44
【问题描述】:

让我明确一点,我在下面描述的方法是可操作的。我希望提高该方法的吞吐量。它有效,而且效果很好。我们正在寻求进一步扩展吞吐量,这就是我正在研究这个的原因。

当前的任务是提高评分算法的性能,该算法返回一组任务的最佳分数。我收集了使用ExecutorService 执行评分的任务。每个任务检查它现在是否有最好的分数,如果它是新的最好的,则以同步的方式更新最好的分数。为了深入了解我正在处理的规模,每个任务都需要不到一毫秒的时间来完成,但是有数千个任务,因此需要数百毫秒才能找到最好的任务。我每分钟执行数百次这个评分算法。结果是 60 秒中有 30 秒用于运行此评分算法。

当我的线程池是 8 个线程(具有 24 个虚拟内核)时,每个任务需要 0.3 毫秒。当我有 20 个线程(同一台机器,24 个虚拟内核)时,每个任务需要 0.6 毫秒。我怀疑当我向我的ExecutorService 线程池添加更多线程时,我的性能会因为最好的同步而变得更糟(更多线程争用锁)。

我进行了大量搜索,但似乎找不到令人满意的(实际上,我似乎找不到任何)替代方案。我正在考虑收集所有分数并按排序顺序存储,或者在所有任务完成后排序——但我不确定这是否会有任何改进。

有没有人想过另一种更有效的方式来收集最好成绩?

这是当前的方法:

final double[] bestScore = { Double.MAX_VALUE };
// for each item in the collection {
    tasks.add(Executors.callable(new Runnable() {
        public void run() {
            double score = //... do the scoring for the task
            if (score < bestScore[0]) {
                synchronized(bestScore) {
                    if (score < bestScore[0]) { // check again after we have the lock
                        bestScore[0] = score;
                        ...
                        // also save off other task identifiers in a similar fashion
                    }
                }
            }
        }
    }
} // end of loop creating scoring tasks

List<Future<Object>> futures = executorService.invokeAll(tasks /*...timeout params here*/);
... // handle cancelled tasks 

// now use the best scoring task that was saved off when it was found.

【问题讨论】:

  • 您是否为每个分值生成一个线程?
  • 我不确定我是否理解这个问题。我认为我使用的线程框架在上面很清楚,请澄清你的问题。
  • tasks 是什么类型? getExecutorService() 返回什么? (我假设它返回一个ExecutorService,但Executors 中的不同方法返回ExecutorService 的不同实现,所以我很好奇它是什么)。
  • List&lt;Callable&lt;Object&gt;&gt; tasks... 是的,getExecutorService() 返回 ExecutorService...
  • 我认为这里的大部分开销可能是同步。你需要它吗?你真的需要数组吗?您可以考虑拥有一个直接更新的 volatile double bestScore 变量。如果您需要所有分数,我可能会将生成的每个分数添加到PriorityQueue,或者可能是随后排序的ConcurrentLinkedList

标签: java multithreading concurrency java-8


【解决方案1】:

我不得不理所当然地认为,您希望将每个单独的分数计算为提交给ExecutorService 的单独任务。必须有其他好处,否则开销不值得。通常,您会实现一个Callable,它在执行时返回分数(或带有分数和其他相关结果的对象)。成功调用所有任务后,将在主线程中检查所有结果以获得最佳结果。

但是,鉴于您的限制,您可以尝试的一种优化是使用 DoubleAccumulator,它适用于此类情况,而不是您的单元素数组和同步。它看起来像这样:

final DoubleAccumulator lowest = new DoubleAccumulator(Math::min, Double.POSITIVE_INFINITY);
/* Loop, creating all the tasks... */
for ( ... ) {
  tasks.add(Executors.callable(new Runnable() {
    public void run()
    {
      double score = 0; /* Compute a real score here. */
      lowest.accumulate(score);
    }
  }));
}
/* Invoke all the tasks, when successful... */
double lowestScore = lowest.get();

如果您需要跟踪分数以外的信息,您可以使用AtomicReference 执行类似的操作,创建一个包含任务标识符、分数和任何其他所需属性的数据对象,并使用its accumulators. 之一

如果您的任务是通过某种递归的、分而治之的方法初始化的,从而产生非阻塞、大小相等的任务,那么并行 Stream 下的 fork-join 框架也可能是一个不错的选择。

不过,我要再次指出,如果更多线程会降低性能,那么衡量更少线程的使用似乎是谨慎的做法。

【讨论】:

  • 感谢您的回答!我最近一直在寻找一个简单的计数管理器的LongAdder(根据文档,这实际上是一个LongAccumulator),所以使用相同的方法很有趣,但是使用了一个自定义的分数对象。我会再调查一下。
  • 嗯。通过简单的基准测试,当前方法和累加器之间根本没有太大区别。我想知道该方法是否应该为ExecutorService 的需要和单独评分的任务找到更好的方法。
  • @jadz 正如我评论 akhil_mittal 的答案并在我自己的开头提到的那样,从可调用对象中返回分数并将它们聚合到调用者中是最好的方法。仅当您无法做到这一点时。
【解决方案2】:

假设您有 10k 分数,您需要找到所有分数之间的最佳分数。将您的 10k 分数除以线程数,因此假设您想要 10 个线程,然后每个线程将得到 1000 .

现在每个线程都可以完全并行地从其 1000 中找到最大值。当返回所有 10 个结果时,您只需从这 10 个结果中找出最大值即可获得总体最大值。

【讨论】:

  • 我熟悉这种方法,它在我系统的其他地方使用。我们目前的方法还有其他一些我们不想失去的好处,这就是为什么我们没有将任务分解成块的原因。我希望找到一种适用于当前线程框架的方法。感谢您的回答!
  • 我已经更新了问题中的代码,以包括计分任务的超时时间,这是我在上面的评论中引用的原因之一。
【解决方案3】:

我没有什么顾虑。在您的代码中,bestScore 只有一个元素,那么为什么需要一个数组?为什么将其值设置为 double 的最大允许值?在那种情况下,它永远是最好的分数,不是吗?

此外,您似乎需要确保所有任务都执行,因为只有这样您才能知道任务中的最佳分数。我建议为每个计算分数的任务创建一个新的Callable,例如:

public class ScoreComputer implements Callable<Double> {
    @Override
    public Double call() throws Exception {
        double score = 0;
        //Compute and return score here.
        return score;
    }
}

然后为每个任务提交一个ScoreComputer,它将返回一个Future&lt;Double&gt;,一旦计算结束就会有结果。然后您可以从所有计算结果中找到最大值,并将其与您现有的最佳分数进行比较。

public static void main(String[] args) throws ExecutionException, InterruptedException {
        double bestScore = Double.MAX_VALUE;
        List<Future<Double>> futures = new ArrayList<>();
        //For each item in collection create a task and set it to run.
        ExecutorService service = Executors.newCachedThreadPool();
        futures.add(service.submit(new ScoreComputer()));

        List<Double> scores = new ArrayList<>();

        for(Future<Double> future : futures) {
            scores.add(future.get());
        }

        Double bestScoreInTasks = Collections.min(scores);
        if(bestScore < bestScoreInTasks) {
            bestScore = bestScoreInTasks;
        }
        System.out.println(bestScore);
    }

我相信这会给你一些想法。此外,您的任务持续时间很短,然后 IMO 使用缓存池在这里很有意义。根据 Java 文档:

newCachedThreadPool() 创建一个创建新线程的线程池 根据需要,但将重用以前构造的线程 可用的。这些池通常会提高 执行许多短期异步任务的程序。

【讨论】:

  • 你不能final double bob = 1; bob = 2 这就是为什么double[]final
  • 此外,任何低于 Double.MAX_VALUE 的分数都将低于初始 bestScore...重新阅读上面的条件。我不认为我每次都想要一个新的缓存线程池,因为这被称为每分钟数百次(用这个上下文更新上面的问题)。
  • IMO 你有很多计算分数的任务,你想从中找到最好的分数。如果这是您想要的,那么您不需要我的代码中的最后一个 if 比较。此外,如果你使用`invokeAll`,它就像Thread.start,如果你的任务需要很多时间,它可能会成为性能杀手。使用缓存线程池仍然是一个更好的解决方案。为什么不在得出任何结论之前对结果进行基准测试?
  • 在阅读完您的回答后,我认为您已经提出了我在上面的问题中暗示我可能会做的事情。您将每个任务单独提交给服务(我构建了一个列表,然后调用All())。然后,您查看最后的结果并找到最好的结果。我不确定这是否会提高性能。部分问题是我想要得分的任务,而不仅仅是得分。我会再次澄清这个问题。
  • 您不必对结果进行排序,只需找到最小值即可。那是 O(n)。至于存储,您已经存储了一堆任务和未来。这应该不多。但很难猜测性能。
【解决方案4】:

由于您不想像 EricF 建议的那样对其进行分块,我建议您实现自己的执行程序,以便为您分块。这仍然允许您将每个分数计算定义为它自己的Runnable(或者更确切地说,我使用自定义功能接口,但想法是一样的)。

首先,让我们先进行速度测试:

ScoreCalculatorOriginal.java(这实际上是您的代码):

public class ScoreCalculatorOriginal {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ArrayList<Callable<Object>> tasks = new ArrayList<>();
        final double[] bestScore = { Double.MAX_VALUE };
        for(int i = 0; i < 100000; i++) {
            tasks.add(Executors.callable(() -> {
                Random random = new Random();
                double score = Math.pow(Math.sin(random.nextDouble()), 2) * Math.pow(Math.cos(random.nextDouble()), 2);
                if (score < bestScore[0]) {
                    synchronized (bestScore) {
                        if (score < bestScore[0]) {
                            bestScore[0] = score;
                        }
                    }
                }
            }));
        }

        long start = System.nanoTime();
        List<Future<Object>> futures = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
                .invokeAll(tasks);
        for(Future<Object> future : futures) {
            future.get();
        }
        long end = System.nanoTime();
        System.out.printf("Calculation took %.3f ms%n", (end - start) / 1e6);
    }
}

8 个线程(我的核心数):

计算耗时 103.358 毫秒

4 个线程:

计算耗时 104.351 毫秒

1 个线程:

计算耗时 102.918 毫秒

它根本无法扩展。

这是我的看法:

ScoreCalculatorFast.java:

public class ScoreCalculatorFast {

    public static void main(String[] args) throws InterruptedException {
        ScoreExecutor executor = new ScoreExecutor(Runtime.getRuntime().availableProcessors());
        List<ScoreExecutor.ScoreJob> jobs = new ArrayList<>();
        for(int i = 0; i < 100000; i++) {
            jobs.add(() -> {
                Random random = new Random();
                return Math.pow(Math.sin(random.nextDouble()), 2) * Math.pow(Math.cos(random.nextDouble()), 2);
            });
        }
        long start = System.nanoTime();
        executor.getBestScore(jobs);
        long end = System.nanoTime();
        System.out.printf("Calculation took %.3f ms%n", (end - start) / 1e6);
    }
}

8 个线程:

计算耗时 19.624 毫秒

4 个线程:

计算耗时 24.275 毫秒

1 个线程:

计算耗时 41.357 毫秒

如您所见,它的速度明显更快,并且随着线程数的增加而扩展。尽管添加更多线程的回报会递减,因为某些工作不能分布在更多线程上,并且在计算的开始和结束时涉及一些同步。随着更高级的分数计算需要更长的时间,您会看到更多线程的更高收益。

现在这里是你的实现:

ScoreExecutor.java:

public class ScoreExecutor {

    /**
     * A job that calculates a score
     */
    public static interface ScoreJob {

        /**
         * Calculate the score
         * @return the calculated score
         */
        double calculateScore();
    }

    // This is the threads that do all the work
    final ArrayList<ScoreThread> threads;

    ScoreExecutor(int numThreads) {
        // Create the threads
        threads = new ArrayList<>();
        for(int i = 0; i < numThreads; i++) {
            threads.add(new ScoreThread());
        }
        // Start them
        for(ScoreThread thread : threads) {
            thread.start();
        }
    }

    /**
     * Execute a collection of ScoreJobs and return the best score among them.
     * @param jobs The jobs to execute
     * @return The best score from the scores calculated by the jobs
     * @throws InterruptedException
     */
    public double getBestScore(Collection<ScoreJob> jobs) throws InterruptedException {
        ArrayList<ScoreJob> jobList = new ArrayList<>(jobs);
        // Start all threads
        int chunkSize = jobList.size() / threads.size();
        for(int i = 0; i < threads.size() - 1; i++) {
            threads.get(i).startJobs(jobList.subList(i * chunkSize, (i+1) * chunkSize));
        }
        // Start the last thread
        int lastIndex = threads.size() - 1;
        threads.get(lastIndex).startJobs(jobList.subList(lastIndex * chunkSize, jobList.size()));

        // Get the best score from each thread
        LinkedList<Double> threadScores = new LinkedList<>();
        for(ScoreThread thread : threads) {
            threadScores.add(thread.getBestScore());
        }
        // Calculate the best score
        double bestScore = Double.MAX_VALUE;
        for(Double score : threadScores) {
            if(score < bestScore) {
                bestScore = score;
            }
        }
        return bestScore;
    }

    /**
     * Worker thread
     */
    private class ScoreThread extends Thread {

        // If we're currently running a score calculation
        private volatile boolean run;

        // The current best score
        private volatile double bestScore;

        // Latch for synchronisation with the executor
        private CountDownLatch latch;

        // The list of jobs to execute
        private final LinkedList<ScoreJob> scoreJobs = new LinkedList<>();

        private void startJobs(Collection<ScoreJob> jobs) {
            synchronized (this) {
                if(!run) {
                    // Start the thread
                    scoreJobs.addAll(jobs);
                    latch = new CountDownLatch(1);
                    run = true;
                    this.notifyAll();
                } else {
                    throw new IllegalStateException("This thread is already running jobs");
                }
            }
        }

        /**
         * Get the best score at the end of the calculation.
         * Waits until all jobs are finished and then returns
         * this thread's best score.
         * @return This threads best score
         * @throws InterruptedException
         */
        private double getBestScore() throws InterruptedException {
            // Wait for completion and return
            latch.await();
            return bestScore;
        }

        @Override
        public void run() {
            run = false;
            try {
                // External loop, run forever so we can run multiple jobs
                while (true) {
                    // Wait for a job to be started
                    synchronized (this) {
                        while (!run) {
                            wait();
                        }
                    }
                    // This threads best score
                    double bestScore = Double.MAX_VALUE;
                    ScoreJob job; // The current job
                    // Get a job
                    while((job = scoreJobs.poll()) != null) {
                        // Calculate the score
                        double score = job.calculateScore();
                        // Update the best score
                        if(score < bestScore) {
                            bestScore = score;
                        }
                    }
                    // We're done, update the best score and release the latch
                    this.bestScore = bestScore;
                    latch.countDown();
                    // Set run to false so we wait for the next batch of jobs
                    run = false;
                }
            } catch(InterruptedException e) {
                e.printStackTrace();
                return;
            }
        }
    }
}

这里的主要优化是每个线程都跟踪它的本地最佳分数,而不是更新全局最佳分数。然后,一旦所有线程都完成,我们让执行器从所有线程的最佳分数中选择最佳分数。

【讨论】:

  • 感谢您非常详细的回答!正如您所建议的,这是 EricF 建议的样式的更详细的答案。我认为通过您展示的具体方法,我仍然可以从我们实现线程的方式中获得好处,但引入了每个线程的优化,以保持本地最佳分数。我很好奇您是否知道为什么原始方法不随线程数扩展。是线程之间的同步竞争吗?
  • 如果我建立在您的基准上并模拟一个对我来说更现实的任务(运行加载计算 2200 次,而不是只运行一次以获得每个任务大约 0.3 毫秒的持续时间),我不会得到的结果和我在实际代码中看到的结果不同。
  • 我应该澄清...“我没有得到相同的结果”我的意思是通过添加更多线程我看到了可接受的扩展。
  • 是的。同步导致您的原始代码无法扩展。请记住,synchronized 块一次只允许一个线程执行该块,所以如果您有 20 个线程在运行,会发生什么情况。其中 19 个线程将等待第一个线程检查它的分数是否是新的最好的,然后才能继续。保持同步作业队列的所有开销(这是我在实现中避免的)这样的解决方案只有在同步块之外的代码需要更长的时间才能执行时才会得到回报。
猜你喜欢
  • 2016-12-21
  • 1970-01-01
  • 1970-01-01
  • 2013-03-17
  • 2018-08-04
  • 1970-01-01
  • 1970-01-01
  • 2023-03-09
  • 1970-01-01
相关资源
最近更新 更多