多线程总是比单线程产生更好的性能吗?

Jas*_*key 12 java multithreading

我知道答案是没有,这里有一个例子,为什么单线程比Java的多线程更快?.

因此,当在线程中处理任务是微不足道的时,创建线程的成本将比分发任务产生更多的开销.这是一个单线程比多线程更快的情况.

问题

  • 是否有更多情况下单个线程比多线程更快?

  • 我们什么时候应该决定放弃多线程,只使用一个线程来实现我们的目标?

虽然问题标记为,但也欢迎在Java之外进行讨论.如果我们能在答案中有一个小例子来解释,那将会很棒.

Jen*_*ack 11

这是一个关于线程及其与实际工作的链接的非常好的问题,这意味着可用的物理CPU及其核心和超线程.

  1. 如果您的CPU具有多个可用核心,则多个线程可能允许您并行执行操作.因此,在一个理想的世界中,例如计算一些素数,使用4个线程可能会快4倍,如果你的CPU有4个可用内核并且你的算法工作真正并行.
  2. 如果在核心可用时启动更多线程,则操作系统的线程管理将花费越来越多的时间在线程交换机中,因此使用CPU的效率会变差.
  3. 如果编译器,CPU缓存和/或运行时意识到您运行多个线程,访问内存中的相同数据区,则以不同的优化模式运行:只要编译/运行时确保只有一个线程访问数据,可以避免过于频繁地将数据写入外部RAM,并可能有效地使用CPU的L1缓存.如果不是:必须激活信号量,并且还经常将缓存数据从L1/L2缓存刷新到RAM.

因此,我从高度并行的多线程中学到的经验是:

  • 如果可能,使用单线程,无共享进程更高效
  • 如果需要线程,请尽可能地分离共享数据访问
  • 如果可能,请勿尝试分配比可用核心更多的加载工作线程

这里有一个小程序(javafx)可以玩.它:

  • 分配一个100.000.000大小的字节数组,填充随机字节
  • 提供一种方法,计算此数组中设置的位数
  • 该方法允许对每个"第n个"字节位进行计数
  • count(0,1)将计算所有字节位
  • count(0,4)将计数0',4',8'字节位,允许并行交错计数

使用MacPro(4核)导致:

  1. 运行一个线程,count(0,1)需要1326ms来计算所有399993625位
  2. 运行两个线程,并行计数(0,2)和计数(1,2)需要920ms
  3. 运行四个线程,需要618ms
  4. 运行八个线程,需要631ms

在此输入图像描述 在此输入图像描述 在此输入图像描述 在此输入图像描述

更改计数方式,例如递增共享整数(AtomicInteger或synchronized)将极大地改变许多线程的性能.

public class MulithreadingEffects extends Application {
    static class ParallelProgressBar extends ProgressBar {
        AtomicInteger myDoneCount = new AtomicInteger();
        int           myTotalCount;
        Timeline      myWhatcher = new Timeline(new KeyFrame(Duration.millis(10), e -> update()));
        BooleanProperty running = new SimpleBooleanProperty(false);

        public void update() {
            setProgress(1.0*myDoneCount.get()/myTotalCount);
            if (myDoneCount.get() >= myTotalCount) {
                myWhatcher.stop();
                myTotalCount = 0;
                running.set(false);
            }
        }

        public boolean isRunning() { return myTotalCount > 0; }
        public BooleanProperty runningProperty() { return running; }

        public void start(int totalCount) {
            myDoneCount.set(0);
            myTotalCount = totalCount;
            setProgress(0.0);
            myWhatcher.setCycleCount(Timeline.INDEFINITE);
            myWhatcher.play();
            running.set(true);
        }

        public void add(int n) {
            myDoneCount.addAndGet(n);
        }
    }

    int mySize = 100000000;
    byte[] inData = new byte[mySize];
    ParallelProgressBar globalProgressBar = new ParallelProgressBar();
    BooleanProperty iamReady = new SimpleBooleanProperty(false);
    AtomicInteger myCounter = new AtomicInteger(0);

    void count(int start, int step) {
        new Thread(""+start){
            public void run() {
                int count = 0;
                int loops = 0;
                for (int i = start; i < mySize; i+=step) {
                    for (int m = 0x80; m > 0; m >>=1) {
                        if ((inData[i] & m) > 0) count++;
                    }
                    if (loops++ > 99) {
                        globalProgressBar.add(loops);
                        loops = 0;
                    }
                }
                myCounter.addAndGet(count);
                globalProgressBar.add(loops);
            }
        }.start();
    }

    void pcount(Label result, int n) {
        result.setText("("+n+")");
        globalProgressBar.start(mySize);
        long start = System.currentTimeMillis();
        myCounter.set(0);
        globalProgressBar.runningProperty().addListener((p,o,v) -> {
            if (!v) {
                long ms = System.currentTimeMillis()-start;
                result.setText(""+ms+" ms ("+myCounter.get()+")");
            }
        });
        for (int t = 0; t < n; t++) count(t, n);
    }

    void testParallel(VBox box) {
        HBox hbox = new HBox();

        Label result = new Label("-");
        for (int i : new int[]{1, 2, 4, 8}) {
            Button run = new Button(""+i);
            run.setOnAction( e -> {
                if (globalProgressBar.isRunning()) return;
                pcount(result, i);
            });
            hbox.getChildren().add(run);
        }

        hbox.getChildren().addAll(result);
        box.getChildren().addAll(globalProgressBar, hbox);
    }


    @Override
    public void start(Stage primaryStage) throws Exception {        
        primaryStage.setTitle("ProgressBar's");

        globalProgressBar.start(mySize);
        new Thread("Prepare"){
            public void run() {
                iamReady.set(false);
                Random random = new Random();
                random.setSeed(4711);
                for (int i = 0; i < mySize; i++) {
                    inData[i] = (byte)random.nextInt(256);
                    globalProgressBar.add(1);
                }
                iamReady.set(true);
            }
        }.start();

        VBox box = new VBox();
        Scene scene = new Scene(box,400,80,Color.WHITE);
        primaryStage.setScene(scene);

        testParallel(box);
        GUIHelper.allowImageDrag(box);

        primaryStage.show();   
    }

    public static void main(String[] args) { launch(args); }
}
Run Code Online (Sandbox Code Playgroud)


ala*_*ain 8

正如 @Jim Mischel 的评论中已经提到的,您可以使用

阿姆达尔定律

来计算这个。阿姆达尔定律指出,通过添加处理器来解决任务所获得的加速为

在此输入图像描述

在哪里

N是处理器的数量,并且

P是可以并行执行的代码的比例 (0 .. 1)

现在,如果T是在单个处理器上执行任务所需的时间,O是总“开销”时间(创建和设置第二个线程、通信……),则单个线程更快,如果

T < T/S(2) + O

或者,重新排序后,如果

O/T > P/2

当开销/执行时间的比率大于P/2时,单线程速度更快。


kap*_*pex 5

并非所有算法都可以并行处理(严格顺序的算法;其中Amdahl定律中P = 0 )或至少不能有效(参见P-complete).其他算法更适合并行执行(极端情况称为"令人尴尬的并行").

与类似的顺序算法相比,并行算法的简单实现在复杂性或空间方面可能效率较低.如果没有明显的方法来并行化算法以使其获得加速,那么您可能需要选择另一个类似的并行算法来解决相同的问题但可能效率更高或更低.如果忽略线程/进程创建和直接进程间通信开销,则在使用IO瓶颈等共享资源或由较高内存消耗引起的增加分页时,仍可能存在其他限制因素.

我们什么时候应该决定放弃多线程,只使用一个线程来实现我们的目标?

在单线程和多线程之间进行决策时,应考虑更改实现所需的时间以及开发人员增加的复杂性.如果使用多个线程只有很小的增益,你可能会认为通常由多线程应用程序引起的增加的维护成本不值得加速.


los*_*kah 3

开销可能不仅用于创建,还用于线程相互通信。应该注意的另一件事是,例如单个对象上的线程同步可能会导致类似的单线程执行。