繁体   English   中英

多线程没有加速 Java 中的进程

[英]Multithreading not speeding up the process in Java

我正在尝试同时运行4 个线程,以希望加快进程(可以并行完成而没有问题)。 目前,每个线程都调用相同的 function(总是花费大致相同的时间),并且线程(几乎)同时启动。 使用打印语句,我检查了每个threadX.join()之间经过了多少时间,并且时间与我连续四次运行 function 的时间完全相同,但在控制面板中我可以看到确实使用了 4 个线程(在它们的开头 4 个,然后一旦第一个加入只有 3 个,然后是两个,等等)。

这是我的代码:

long start = System.currentTimeMillis();

        Foo thread01 = new Foo();
        Foo thread02 = new Foo();
        Foo thread03 = new Foo();
        Foo thread04 = new Foo();

        System.out.println("Created all thread0x");

        Thread thread1 = new Thread(thread01);
        Thread thread2 = new Thread(thread02);
        Thread thread3 = new Thread(thread03);
        Thread thread4 = new Thread(thread04);

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();

        try {
            thread1.join();
            thread2.join();
            thread3.join();
            thread4.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        double value1 = thread01.getResult();
        double value2 = thread02.getResult();
        double value3 = thread03.getResult();
        double value4 = thread04.getResult();

        System.out.println("Result1: " + value1);
        System.out.println("Result2: " + value2);
        System.out.println("Result3: " + value3);
        System.out.println("Result4: " + value4);

        long end = System.currentTimeMillis();
        long elapsedTime = end - start;
        System.out.println("Elapsed time: " + elapsedTime / 1000.0);

我做错了什么,或者我不明白什么?

这里是测试 class 的代码。

public class Foo implements Runnable {
    private double result;

    @Override
    public void run() {
        fakeMiniMax(0);
    }

    public double fakeMiniMax(int iterationNumber) {
        int total = 0;

        if (iterationNumber > 6) {
            this.result = 100;
            return total;
        }
        else {
            this.result = -1;
            return this.fakeMiniMax(iterationNumber + 1) + this.fakeMiniMax(iterationNumber + 1) - this.fakeMiniMax(iterationNumber + 1) + this.fakeMiniMax(iterationNumber + 1) / 20 +  this.fakeMiniMax(iterationNumber + 1) - 45 -  this.fakeMiniMax(iterationNumber + 1) - this.fakeMiniMax(iterationNumber + 1) -  this.fakeMiniMax(iterationNumber + 1) -  this.fakeMiniMax(iterationNumber + 1) -  this.fakeMiniMax(iterationNumber + 1) +  this.fakeMiniMax(iterationNumber + 1) +  this.fakeMiniMax(iterationNumber + 1) + this.fakeMiniMax(iterationNumber + 1) + this.fakeMiniMax(iterationNumber + 1) - this.fakeMiniMax(iterationNumber + 1) -  this.fakeMiniMax(iterationNumber + 1) - this.fakeMiniMax(iterationNumber + 1) -  this.fakeMiniMax(iterationNumber + 1) - this.fakeMiniMax(iterationNumber + 1) -  this.fakeMiniMax(iterationNumber + 1) - this.fakeMiniMax(iterationNumber + 1) -  this.fakeMiniMax(iterationNumber + 1) + this.fakeMiniMax(iterationNumber + 1) +  this.fakeMiniMax(iterationNumber + 1) + this.fakeMiniMax(iterationNumber + 1) +  this.fakeMiniMax(iterationNumber + 1) + this.fakeMiniMax(iterationNumber + 1) +  this.fakeMiniMax(iterationNumber + 1) + this.fakeMiniMax(iterationNumber + 1) +  this.fakeMiniMax(iterationNumber + 1) + this.fakeMiniMax(iterationNumber + 1) +  this.fakeMiniMax(iterationNumber + 1);
        }
    }

    double getResult() {
        return result;
    }
}

我的硬件:

  • 4核
  • 8 个线程
  • 16GB 内存
  • 该任务仅持续使用 CPU,直到该方法返回其结果。
  • 任务的代码都写在上面

tl;博士

我也看到奇怪的结果,同时运行你的递归任务需要更多的时间,而不是更少。 我不知道为什么。

细节

乍一看,您的代码看起来是正确的,尽管我没有研究递归调用。 (小问题:我的 IDE 说您的FakeMiniMax#getResult方法永远不会被调用。)

执行者框架

但是……你的代码已经过时了。 在现代 Java 中,我们很少需要直接处理Thread class。 相反,我们使用添加到 Java 5 的Executors框架。该框架使我们摆脱了杂耍线程和管理线程池的苦差事。

正如markspace 所评论的,使用System.nanoTime以获得更准确的基准计时。 如果您真的想认真对待基准测试,请使用Java Microbenchmark Harness (JMH) 此外,使用Duration来跟踪经过的时间,而不是进行数学运算 ( /1_000 )。

Callable

你想收集一个结果。 所以我们使用Callable而不是Runnable

Double回object

让我们使用 object Double作为递归方法的返回值,而不是原始的double 我们必须这样做来参数化Callable的类型。

Future对象

当向执行器服务提交任务时,我们会返回一个Future object。 此 object 可用于跟踪完成状态。 我们将从这个Future object: a Double object 中检索任务的结果。 当我们将多个任务提交给执行器服务时,我们会收集返回给我们的每个Future object。

我们可以将多个任务提交给一个执行器服务,然后等待它们完成。 当前执行此操作的方法是首先调用ExecutorService#shutdown ,然后调用awaitTermination并超时。 当提交的任务完成或取消时,控制流将超出awaitTermination调用。

示例代码

这是我的FakeMiniMax class 的修订版。

package work.basil.demo.threadmark;

import java.time.Instant;
import java.util.concurrent.Callable;

public class FakeMiniMax implements Callable < Double >
{
    private double result;

    // `Callable` interface
    @Override
    public Double call ( ) throws Exception
    {
        System.out.println( "INFO - The `call` method of `FakeMiniMax` is starting to run in thread ID " + Thread.currentThread().getId() + " at " + Instant.now() + "." );
        return fakeMiniMax( 0 );
    }

    public double fakeMiniMax ( int iterationNumber )
    {
        int total = 0;

        if ( iterationNumber > 6 )
        {
            this.result = 100;
            return total;
        } else
        {
            this.result = - 1;
            return this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) / 20 + this.fakeMiniMax( iterationNumber + 1 ) - 45 - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) - this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 ) + this.fakeMiniMax( iterationNumber + 1 );
        }
    }

    double getResult ( )
    {
        return result;
    }
}

这是用作测试工具的App class。

package work.basil.demo.threadmark;

import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.*;

public class App
{
    public static void main ( String[] args )
    {
        System.out.println( "INFO - The `main` method of `App` is starting to run in thread ID " + Thread.currentThread().getId() + " at " + Instant.now() + "." );
        long start = System.nanoTime();

        ExecutorService executorService = Executors.newFixedThreadPool( 4 );  // Executors.newSingleThreadExecutor() or  Executors.newFixedThreadPool( 4 )

        // Populate a list of tasks to be executed on background threads.
        int countTasks = 4;
        List < FakeMiniMax > tasks = new ArrayList <>( countTasks );
        for ( int nthTask = 0 ; nthTask < countTasks ; nthTask++ )
        {
            tasks.add( new FakeMiniMax() );
        }

        // Submit list of tasks to executor service. Collect each task’s `Future` object.
        List < Future < Double > > futures = null;
        try
        {
            futures = executorService.invokeAll( tasks );
            executorService.shutdown(); // Prevents adding any more tasks.
            try { executorService.awaitTermination( Duration.ofSeconds( 10 ).toMillis() , TimeUnit.MILLISECONDS ); } catch ( InterruptedException e ) { e.printStackTrace(); }

            // Review the list of `Future` objects collected for each of all the tasks.
            for ( Future < Double > future : futures )
            {
                try { System.out.println( "future.get() = " + future.get() ); } catch ( InterruptedException e ) { e.printStackTrace(); } catch ( ExecutionException e ) { e.printStackTrace(); }
            }
        }
        catch ( InterruptedException e )
        {
            e.printStackTrace();
        }
        finally
        {
            if ( Objects.nonNull( executorService ) && ! executorService.isShutdown() )
            {
                executorService.shutdownNow(); // Prevents adding any more tasks.
            }
        }

        Duration elapsed = Duration.ofNanos( System.nanoTime() - start );
        System.out.println( "Our " + countTasks + " tasks took " + elapsed + "." );
        System.out.println( "INFO - The `main` method of `App` is ending its run in thread ID " + Thread.currentThread().getId() + " at " + Instant.now() + "." );
    }
}

请注意,我们可以替换我们选择的ExecutorService实现。 我们可以使用单线程执行器服务来仅使用单个内核按顺序运行我们的任务。 或者我们可以切换到由固定线程池支持的多线程执行器服务。 只需更改一行代码即可在两种模式下自行运行此测试。

试验结果

When I run this on a fairly busy Mac mini ( way too many web browser windows & tabs open,) with an Intel chip containing six cores but no Hyper-Threading, 32 gigs RAM, and Java 16. I get the following results.

单线程

使用单个线程,经过的时间约为 2.5 分钟。

请注意时间戳如何显示随时间顺序运行的每个call方法。 第二个在第一个完成之前不会开始,依此类推。

控制台 output:

INFO - The `main` method of `App` is starting to run in thread ID 1 at 2021-05-09T06:05:39.348246Z.
INFO - The `call` method of `FakeMiniMax` is starting to run in thread ID 14 at 2021-05-09T06:05:39.417155Z.
INFO - The `call` method of `FakeMiniMax` is starting to run in thread ID 14 at 2021-05-09T06:06:30.348844Z.
INFO - The `call` method of `FakeMiniMax` is starting to run in thread ID 14 at 2021-05-09T06:07:07.385369Z.
INFO - The `call` method of `FakeMiniMax` is starting to run in thread ID 14 at 2021-05-09T06:07:43.701939Z.
future.get() = -53874.172111640604
future.get() = -53874.172111640604
future.get() = -53874.172111640604
future.get() = -53874.172111640604
Our 4 tasks took PT2M38.344593758S.
INFO - The `main` method of `App` is ending its run in thread ID 1 at 2021-05-09T06:08:17.737893Z.

四个线程

使用四个线程的线程池,经过的时间是 7 分钟!

我不知道为什么这需要更多时间而不是单线程时间,7 分钟而不是 2.5 分钟。

请注意时间戳如何显示在同一时间、同一毫秒内开始的所有四个任务。 然后时间流逝,因为所有四个同时进行计算工作。

控制台 output:

INFO - The `main` method of `App` is starting to run in thread ID 1 at 2021-05-09T05:52:53.511070Z.
INFO - The `call` method of `FakeMiniMax` is starting to run in thread ID 15 at 2021-05-09T05:52:53.540791Z.
INFO - The `call` method of `FakeMiniMax` is starting to run in thread ID 14 at 2021-05-09T05:52:53.540791Z.
INFO - The `call` method of `FakeMiniMax` is starting to run in thread ID 16 at 2021-05-09T05:52:53.541806Z.
INFO - The `call` method of `FakeMiniMax` is starting to run in thread ID 17 at 2021-05-09T05:52:53.541910Z.
future.get() = -53874.172111640604
future.get() = -53874.172111640604
future.get() = -53874.172111640604
future.get() = -53874.172111640604
Our 4 tasks took PT7M6.872664491S.
INFO - The `main` method of `App` is ending its run in thread ID 1 at 2021-05-09T06:00:00.406725Z.

结论

在我运行你的 CPU 密集型代码的修订版本的实验中,我得到了疯狂的结果。 并发不是节省时间,而是需要更多时间。

我对这些结果感到困惑。 我预计使用 4 个线程和 6 个内核将时间缩短 3/4,但我发现时间增加了一倍以上。 我至少运行了这个测试十几次。 对于单线程模式,我可以看到我的六个核心中的一个在整个时间内都跃升至接近 100% 的利用率。 在多线程模式下,我清楚地看到所有 6 个内核(不是 4 个)从 10-20% 的利用率跃升至大约 70-90% 的利用率。 因此,这些线程当然似乎是同时跨内核运行的。

我希望有人可以研究此代码以提供解释。

我唯一没有受过教育的疯狂想法:在我的 3 GHz Intel Core i5 芯片上是否只有一个浮点单元 (FPU)在内核之间共享,因此成为所有递归double数学运算的瓶颈? 即使是这样,为什么这么慢?


提示:请注意我们如何在每次调用System.out.println时包含对Instant.now的调用。 研究这些时间戳! 跨线程访问System.out时,output 可能会以意想不到的方式交错。 永远不要假设控制台的连续行是按时间顺序发生的。

Thread.join是一个阻塞操作。 线程有可能按照您调用的顺序执行join 无论如何,在 Java 中手动处理线程并不是最好的选择。 有更好的选择。 在您的情况下,我建议您尝试FutureCompletionService 这是一个例子

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM