【问题标题】:Is it possible to use multithreading without creating Threads over and over again?是否可以在不一遍又一遍地创建线程的情况下使用多线程?
【发布时间】:2011-02-15 17:09:27
【问题描述】:

首先,再次感谢所有已经回答我问题的人。我不是一个很有经验的程序员,这是我第一次接触多线程。

我得到了一个与我的问题非常相似的示例。我希望它可以减轻我们在这里的情况。

public class ThreadMeasuring {
private static final int TASK_TIME = 1; //microseconds
private static class Batch implements Runnable {
    CountDownLatch countDown;
    public Batch(CountDownLatch countDown) {
        this.countDown = countDown;
    }

    @Override
    public void run() {         
        long t0 =System.nanoTime();
        long t = 0;
        while(t<TASK_TIME*1e6){ t = System.nanoTime() - t0; }

        if(countDown!=null) countDown.countDown();
    }
}

public static void main(String[] args) {
    ThreadFactory threadFactory = new ThreadFactory() {
        int counter = 1;
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "Executor thread " + (counter++));
            return t;
        }
    };

  // the total duty to be divided in tasks is fixed (problem dependent). 
  // Increase ntasks will mean decrease the task time proportionally. 
  // 4 Is an arbitrary example.
  // This tasks will be executed thousands of times, inside a loop alternating 
  // with serial processing that needs their result and prepare the next ones.
    int ntasks = 4; 
    int nthreads = 2;
    int ncores = Runtime.getRuntime().availableProcessors();
    if (nthreads<ncores) ncores = nthreads;     

    Batch serial = new Batch(null);
    long serialTime = System.nanoTime();
    serial.run();
    serialTime = System.nanoTime() - serialTime;

    ExecutorService executor = Executors.newFixedThreadPool( nthreads, threadFactory );
    CountDownLatch countDown = new CountDownLatch(ntasks);

    ArrayList<Batch> batches = new ArrayList<Batch>();
    for (int i = 0; i < ntasks; i++) {
        batches.add(new Batch(countDown));
    }

    long start = System.nanoTime();
    for (Batch r : batches){
        executor.execute(r);
    }

    // wait for all threads to finish their task
    try {
        countDown.await();
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    long tmeasured = (System.nanoTime() - start);

    System.out.println("Task time= " + TASK_TIME + " ms");
    System.out.println("Number of tasks= " + ntasks);
    System.out.println("Number of threads= " + nthreads);
    System.out.println("Number of cores= " + ncores);
    System.out.println("Measured time= " + tmeasured);
    System.out.println("Theoretical serial time= " + TASK_TIME*1000000*ntasks);
    System.out.println("Theoretical parallel time= " + (TASK_TIME*1000000*ntasks)/ncores);
    System.out.println("Speedup= " + (serialTime*ntasks)/(double)tmeasured);

    executor.shutdown();
}
 }

每个批次不进行计算,而是等待给定的时间。程序计算 加速,理论上它总是 2,但如果“TASK_TIME”很小,则可以得到小于 1(实际上是减速)。

我的计算需要 1 毫秒,通常更快。在 1 毫秒内,我发现大约 30% 的加速,但在实践中,使用我的程序,我注意到 减速

这段代码的结构和我的程序很相似,如果你能帮助我优化线程处理,我将不胜感激。

亲切的问候。

下面,原来的问题:

嗨。

我想在我的程序中使用多线程,因为我相信它可以大大提高它的效率。它的大部分运行时间是由于独立计算。

我的程序有数千个独立的计算(要解决几个线性系统),但它们只是由几十个左右的小团体同时发生。这些组中的每一个都需要几毫秒才能运行。在其中一组计算之后,程序必须按顺序运行一段时间,然后我必须再次求解线性系统。

实际上,可以将这些要求解的独立线性系统视为在一个循环中,该循环迭代数千次,与依赖于先前结果的顺序计算交替进行。我加快程序速度的想法是在并行线程中计算这些独立计算,方法是将每个组划分为(我可用的处理器数量)批次的独立计算。所以,原则上根本不用排队。

我尝试使用 FixedThreadPool 和 CachedThreadPool,它甚至比串行处理还要慢。每次我需要解决批次时,似乎都需要花费太多时间来创建新的 Treads。

有没有更好的方法来处理这个问题?我使用的这些池似乎适用于每个线程需要更多时间而不是数千个较小线程的情况......

谢谢! 最好的问候!

【问题讨论】:

  • 可以贴一些代码吗?如果您使用固定线程池,则它不会一遍又一遍地创建线程(它们被重用)。
  • 你在什么平台上运行这个?多核服务器与 5 年前的黑莓之间的巨大差异。
  • @ursoouindio 我提出了一个带有阻塞队列的生产者/消费者模式,请查看我的答案以获取更多详细信息。
  • @Jeff:我必须创建一个类似的示例,因为我的代码依赖于几个类。我相信线程创建问题是由于并行和顺序部分的交替。实际上,并行部分只是程序代码的一小部分。
  • @MusiGenesis,我在两台机器上使用 Ubuntu Linux 10.04:Core 2 Duo T7250 和 Core 2 Quad Q6600。

标签: java multithreading threadpool


【解决方案1】:

线程池不会一遍又一遍地创建新线程。这就是为什么它们是游泳池。

您使用了多少线程以及您有多少 CPU/内核?系统负载是什么样的(通常,当您连续执行它们时,以及当您使用池执行时)?是否涉及同步或任何类型的锁定?

并行执行的算法与串行执行的算法是否完全相同(您的描述似乎表明串行正在重用先前迭代的某些结果)。

【讨论】:

  • CachedThreadPools 将停止闲置 60 秒的未使用线程,但正如@Konrad 建议的那样,固定线程池会创建它创建的线程(无论是否使用),直到它被终止。跨度>
  • @Peter 是正确的。我不认为值得一提,因为 OP 提到了批量计算。我估计一个批处理是尽可能快地执行的,所以没有办法闲置 60 秒。
  • 感谢@Konrad 的回复。是的,它不是池错误的线程数,而是我如何在代码中管理它们。对于每个并行批处理 r,我调用了数千次 executor.execute(r)(并且执行程序只声明了一次:executor = Executors.newFixedThreadPool(numberOfCores);)。据我了解,它们成为新线程(如果我错了,请纠正我)。我在两台机器上运行 Ubuntu 10.04,一台是双核,另一台是四核。我用线程数=核心数。
  • 关于系统负载,现在测试我的两个处理器在多线程上都在 70% 左右,在串行运行上只有一个在 90% 左右。我进行了 3 次运行测试,它们略有不同。不需要任何类型的同步或锁定,除了在一组批次结束时,多线程应该保持到下一个机会,并且顺序串行计算需要完成批次。
  • 使用更多的线程怎么样?像 numberOfProcessors+2 一样?您是否为它实例化了数千个RunnablesCallables
【解决方案2】:

从我读到的内容:“数千个独立计算......同时发生......运行需要几毫秒”在我看来,您的问题非常适合 GPU 编程。

我认为它回答了你的问题。 GPU 编程正变得越来越流行。有用于 CUDA 和 OpenCL 的 Java 绑定。如果你可以使用它,我说去吧。

【讨论】:

  • 谢谢,@zvezdi!我会看看那个。听起来很有趣。
【解决方案3】:

我不确定您是如何执行计算的,但如果您将它们分成小组,那么您的应用程序可能适合生产者/消费者模式。

此外,您可能有兴趣使用BlockingQueue。计算消费者将阻塞直到队列中有东西并且阻塞发生在take()调用上。

private static class Batch implements Runnable {
    CountDownLatch countDown;
    public Batch(CountDownLatch countDown) {
        this.countDown = countDown;
    }

    CountDownLatch getLatch(){
        return countDown;
    }

    @Override
    public void run() {         
        long t0 =System.nanoTime();
        long t = 0;
        while(t<TASK_TIME*1e6){ t = System.nanoTime() - t0; }

        if(countDown!=null) countDown.countDown();
    }
}

class CalcProducer implements Runnable {
    private final BlockingQueue queue;
    CalcProducer(BlockingQueue q) { queue = q; }
    public void run() {
        try {
            while(true) { 
                CountDownLatch latch = new CountDownLatch(ntasks);
                for(int i = 0; i < ntasks; i++) {
                    queue.put(produce(latch)); 
                }
                // don't need to wait for the latch, only consumers wait
            }
        } catch (InterruptedException ex) { ... handle ...}
    }

    CalcGroup produce(CountDownLatch latch) {
        return new Batch(latch);
    }
}

class CalcConsumer implements Runnable {
    private final BlockingQueue queue;

    CalcConsumer(BlockingQueue q) { queue = q; }

    public void run() {
        try {
            while(true) { consume(queue.take()); }
        } catch (InterruptedException ex) { ... handle ...}
    }

    void consume(Batch batch) { 
        batch.Run();
        batch.getLatch().await();
    }
}

class Setup {
    void main() {
        BlockingQueue<Batch> q = new LinkedBlockingQueue<Batch>();
        int numConsumers = 4;

        CalcProducer p = new CalcProducer(q);
        Thread producerThread = new Thread(p);
        producerThread.start();

        Thread[] consumerThreads = new Thread[numConsumers];

        for(int i = 0; i < numConsumers; i++)
        {
            consumerThreads[i] = new Thread(new CalcConsumer(q));
            consumerThreads[i].start();
        }
    }
}

对不起,如果有任何语法错误,我一直在学习 C# 代码,有时我忘记了正确的 java 语法,但总体思路就在那里。

【讨论】:

  • +1 比我上面的答案更精细(也可能更正确)的实现。
  • 谢谢,@Lirik!我会花时间理解你的想法。
  • @ursoouindio 让我知道是否有一些令人困惑的事情......基本概念类似于快餐店:您有一行(排队)客户并且您有多个收银员。收银员是空闲的,直到有人排队,然后下一个可用的收银员叫客户离开队列并为他们服务。客户是生产者,收银员是消费者。
  • 我遵循这一点,@Lirik。我只是错过了 CalcGroup 课程,但我相信这不是这个想法的重要部分。我已经用代码更新了这个问题,你能帮我把你的想法应用到那个问题上吗? (如果你认为它会有效)
  • @ursoouindio 我不知道您将这组计算称为Batch(顺便说一句)所以我使用了一个虚拟名称CalcGroup,所以CalcGroup 被@987654327 替换@。考虑到这一点,我已经更新了我的答案:请注意,虽然这可以很好地工作,但等待 CountDownLatch 并不是使用此模式的最佳方式。
【解决方案4】:

如果您遇到无法扩展到多核的问题,则需要更改程序,或者遇到的问题不像您想象的那样并行。我怀疑你有其他类型的错误,但不能根据给出的信息说。

此测试代码可能会有所帮助。

Time per million tasks 765 ms

代码

ExecutorService es = Executors.newFixedThreadPool(4);
Runnable task = new Runnable() {
    @Override
    public void run() {
        // do nothing.
    }
};
long start = System.nanoTime();
for(int i=0;i<1000*1000;i++) {
    es.submit(task);
}
es.shutdown();
es.awaitTermination(10, TimeUnit.SECONDS);
long time = System.nanoTime() - start;
System.out.println("Time per million tasks "+time/1000/1000+" ms");

编辑:假设您有一个循环执行此操作。

for(int i=0;i<1000*1000;i++)
    doWork(i);

您可能认为像这样更改为循环会更快,但问题是开销可能大于增益。

for(int i=0;i<1000*1000;i++) {
    final int i2 = i;
    ex.execute(new Runnable() {
        public void run() {
            doWork(i2);
        }
    }
}

因此,您需要创建一批工作(每个线程至少一个),以便有足够的任务让所有线程保持忙碌,但又不能有太多任务让您的线程花费时间在开销上。

final int batchSize = 10*1000;
for(int i=0;i<1000*1000;i+=batchSize) {
    final int i2 = i;
    ex.execute(new Runnable() {
        public void run() {
            for(int i3=i2;i3<i2+batchSize;i3++)
               doWork(i3);
        }
    }
}

EDIT2:在线程之间复制数据的运行测试。

for (int i = 0; i < 20; i++) {
    ExecutorService es = Executors.newFixedThreadPool(1);
    final double[] d = new double[4 * 1024];
    Arrays.fill(d, 1);
    final double[] d2 = new double[4 * 1024];
    es.submit(new Runnable() {
        @Override
        public void run() {
            // nothing.
        }
    }).get();
    long start = System.nanoTime();
    es.submit(new Runnable() {
        @Override
        public void run() {
            synchronized (d) {
                System.arraycopy(d, 0, d2, 0, d.length);
            }
        }
    });
    es.shutdown();
    es.awaitTermination(10, TimeUnit.SECONDS);
    // get a the values in d2.
    for (double x : d2) ;
    long time = System.nanoTime() - start;
    System.out.printf("Time to pass %,d doubles to another thread and back was %,d ns.%n", d.length, time);
}

开始很糟糕,但升温到约 50 us。

Time to pass 4,096 doubles to another thread and back was 1,098,045 ns.
Time to pass 4,096 doubles to another thread and back was 171,949 ns.
 ... deleted ...
Time to pass 4,096 doubles to another thread and back was 50,566 ns.
Time to pass 4,096 doubles to another thread and back was 49,937 ns.

【讨论】:

  • @Peter,您认为这可能是什么其他错误?如果我无法根据我提供的描述性信息找到解决方案,我会尝试提出一个有代表性的示例。
  • ExecutorService es = Executors.newFixedThreadPool(1); 的结果:Time per million tasks 1435 ms
  • ExecutorService es = Executors.newFixedThreadPool(4); 的结果:Time per million tasks 1561 ms
  • * 在四核 Ubuntu Linux 10.04 上。试了 3 次,得到了较小的值。
  • 这正是我得到的结果。多线程比串行慢一点。
【解决方案5】:

嗯,CachedThreadPool 似乎是为您的情况而创建的。如果您尽快重用它们,它不会重新创建线程,并且如果您在使用新线程之前花费整整一分钟,则线程创建的开销相对可以忽略不计。

但是,除非您还可以并行访问数据,否则您不能期望并行执行能够加快计算速度。如果您使用广泛的锁定、许多同步方法等,您将花费更多的开销而不是并行处理的收益。检查您的数据是否可以有效地并行处理,并且您在代码中没有不明显的同步 lurkinb。

此外,如果数据完全适合缓存,CPU 会有效地处理数据。如果每个线程的数据集大于缓存的一半,则两个线程将竞争缓存并发出许多 RAM 读取,而一个线程如果只使用一个内核,可能会性能更好,因为它避免了在其执行的紧密循环中读取 RAM。也检查一下。

【讨论】:

  • 感谢您的回答!就我而言,它永远不需要等待 60 秒才能进入并行部分。这是我的程序中最常见的部分。即使计划始终使用相同数量的并行线程,您认为 CachedThreadPool 是否更好?
  • 关于锁定和同步,这不是我的情况,因为我的程序在这个意义上具有更简单的结构。每个线程只需要双数组。他们必须在每批中计算多达 2000 个元素。
【解决方案6】:

这是我在想什么的伪大纲

class WorkerThread extends Thread {

    Queue<Calculation> calcs;
    MainCalculator mainCalc;

    public void run() {
        while(true) {
            while(calcs.isEmpty()) sleep(500); // busy waiting? Context switching probably won't be so bad.
            Calculation calc = calcs.pop(); // is it pop to get and remove? you'll have to look
            CalculationResult result = calc.calc();
            mainCalc.returnResultFor(calc,result);      
        }
    }


}

另一个选项,如果您正在调用外部程序。不要将它们放在一个循环中,一次只执行一个,否则它们将不会并行运行。您可以将它们放在一个循环中,一次处理一个,但不能一次执行一个。

Process calc1 = Runtime.getRuntime.exec("myCalc paramA1 paramA2 paramA3");
Process calc2 = Runtime.getRuntime.exec("myCalc paramB1 paramB2 paramB3");
Process calc3 = Runtime.getRuntime.exec("myCalc paramC1 paramC2 paramC3");
Process calc4 = Runtime.getRuntime.exec("myCalc paramD1 paramD2 paramD3");

calc1.waitFor();
calc2.waitFor();
calc3.waitFor();
calc4.waitFor();

InputStream is1 = calc1.getInputStream();
InputStreamReader isr1 = new InputStreamReader(is1);
BufferedReader br1 = new BufferedReader(isr1);
String resultStr1 = br1.nextLine();

InputStream is2 = calc2.getInputStream();
InputStreamReader isr2 = new InputStreamReader(is2);
BufferedReader br2 = new BufferedReader(isr2);
String resultStr2 = br2.nextLine();

InputStream is3 = calc3.getInputStream();
InputStreamReader isr3 = new InputStreamReader(is3);
BufferedReader br3 = new BufferedReader(isr3);
String resultStr3 = br3.nextLine();

InputStream is4 = calc4.getInputStream();
InputStreamReader isr4 = new InputStreamReader(is4);
BufferedReader br4 = new BufferedReader(isr4);
String resultStr4 = br4.nextLine();

【讨论】:

  • 谢谢@glowcoder!我没有在我的程序中使用队列,如果我有 4 个内核,我只需创建 4 个批次并调用 4 个 Runnables,解决它们并遵循代码的顺序部分,直到它再次需要独立计算。
  • 这就是我要说的。如果您将队列添加到您的线程,您可以使用相同的线程并在它们可用时对其进行计算以解决它们。然后你的 mainCalc 可以等待取回计算结果。
  • 好的,我想我明白你的意思了。在我的代码中,可以通过 c 代码(通过 JNI 实现)调用独立计算。我将不得不考虑一下以应用这个想法,但是目前我的计划中没有大的重构。还有其他事情,我不能extend Thread,因为并行例程扩展了我自己的基本类......
  • 关于 JNI:你也可以使用外部进程而不是使用线程。我会将这个概念编辑到帖子中。关于扩展线程:您可以随时implements RunnableThread worker = new Thread(new Worker())
  • 实际上,JNI 代码在主循环中运行,它调用了几个 java 方法,其中一些我正在尝试并行化。
猜你喜欢
  • 1970-01-01
  • 2017-08-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多