2017-10-04 91 views
0

我在使用多线程java程序遇到麻烦。 该程序由多线程整数数组和一个切片总和组成。 问题是计算时间不会通过增加线程数递减(我知道在计算时间比线程少的线程之后线程数有限)。我希望看到在限制线程数量之前执行时间的减少(并行执行的好处)。我在run方法中使用变量假使时间“可读”。多线程编程没有预期的结果

public class MainClass { 

private final int MAX_THREAD = 8; 
private final int ARRAY_SIZE = 1000000; 

private int[] array; 
private SimpleThread[] threads; 
private int numThread = 1; 
private int[] sum; 
private int start = 0; 
private int totalSum = 0; 
long begin, end; 
int fake; 


MainClass() { 
    fillArray(); 

    for(int i = 0; i < MAX_THREAD; i++) { 
     threads = new SimpleThread[numThread]; 
     sum = new int[numThread]; 

     begin = (long) System.currentTimeMillis(); 

     for(int j = 0 ; j < numThread; j++) { 
      threads[j] = new SimpleThread(start, ARRAY_SIZE/numThread, j); 
      threads[j].start(); 
      start+= ARRAY_SIZE/numThread; 
     } 



     for(int k = 0; k < numThread; k++) { 
      try { 
       threads[k].join(); 
      } catch (InterruptedException e) { 
       e.printStackTrace(); 
      } 
     } 


     end = (long) System.currentTimeMillis(); 


     for(int g = 0; g < numThread; g++) { 
      totalSum+=sum[g]; 
     } 


     System.out.printf("Result with %d thread-- Sum = %d Time = %d\n", numThread, totalSum, end-begin); 
     numThread++; 
     start = 0; 
     totalSum = 0; 
    } 

} 


public static void main(String args[]) { 
    new MainClass(); 
} 


private void fillArray() { 
    array = new int[ARRAY_SIZE]; 
    for(int i = 0; i < ARRAY_SIZE; i++) 
     array[i] = 1; 
} 


private class SimpleThread extends Thread{ 
    int start; 
    int size; 
    int index; 

    public SimpleThread(int start, int size, int sumIndex) { 
     this.start = start; 
     this.size = size; 
     this.index = sumIndex; 
    } 

    public void run() { 
     for(int i = start; i < start+size; i++) 
      sum[index]+=array[i]; 

     for(long i = 0; i < 1000000000; i++) { 
      fake++; 
     } 
    } 
} 

Unexpected Result Screenshot

+0

'ARRAY_SIZE/numThread'可能有小数部分,其获取所以'start'变量失去了一些,因此总和可能小于'1000000',这取决于除数的值。 – Griffin

+0

不看细节,但考虑使用[ForkJoinPool](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ForkJoinPool.html)进行此类操作在Java 7+上。将为您节省一些低级别的麻烦。 – Mena

回答

0

作为一般规则,如果每个线程执行的“工作”小于使用线程的开销,则不会从多线程获得加速。

其中一项开销是启动新线程的成本。这是惊人的高。每次启动线程时,JVM都需要执行系统调用来分配线程堆栈内存段和“红色区域”内存段,并对它们进行初始化。 (默认的线程堆栈大小通常为500KB或1MB。)然后还有系统调用来创建本地线程并对其进行调度。

在此示例中,您有1,000,000个元素进行求和,并将此工作分为N个线程。随着N增加,每个线程执行的工作量减少。

不难看出,总计1,000,000个元素花费的时间将少于启动4个线程所需的时间......只是基于对内存读写操作进行计数。然后你需要考虑到父线程一次创建一个子线程。

如果你完全做了分析,很明显,即使你有足够的内核来并行运行所有的线程,添加更多线程实际上会减慢计算速度。而你的基准似乎暗示那个点大约是2个线程。


顺便说一句,还有第二个原因,你想到了这样一个标杆,你可能无法得到尽可能多的加速。每个线程正在做的“工作”基本上是扫描一个大阵列。读取和写入数组将产生对存储器系统的请求。理想情况下,这些请求将由(快速)片上内存缓存满足。但是,如果尝试读取/写入大于内存缓存的数组,则很多/大多数请求会变成(缓慢)主内存请求。更糟糕的是,如果你有N个内核都这样做,那么你可以发现主内存请求的数量太多,内存系统不能跟上...并且线程减速。


底线是多线程不会自动使应用程序更快,如果以错误的方式执行,它肯定不会。

在您的例子:

  • 创建和启动线程的开销相比,每个线程的工作量太小,和
  • 内存带宽的影响很可能是一个问题,如果能“因素走出”线程创建开销

1 - 我不明白的点‘假’的计算。它可能会使基准无效,但JIT编译器可能会优化它。

+0

你是什么意思的线程创建“红区”?我研究了线程与他们所属的进程共享代码,文件和数据。我试图增加数组的大小,现在它的工作!,我有一个双核心CPU与4个线程,我看到计算速度更快,直到3个线程(我认为,因为主要方法本身是一个线程,所以3个线程创建主加上主体本身)。 –

+0

阅读以了解红色区域是什么:https://docs.oracle.com/cd/E19455-01/806-5257/attrib-33670/index。html –

+0

你真的很有帮助,非常感谢你! –

0

启动线程是沉重的,你会只看到在不相同的资源竞争的大型进程(它没有一个适用于这里),它的好处。

0

有时为什么有错?

因为ARRAY_SIZE/numThread可以具有小数部分(例如三分之百万= 333333.3333333333)它获取向下舍入,以便start变量失去一些因此总和可能小于1000000取决于除数的值。

为什么随着线程数量的增加,所花的时间越来越多?

因为每个线程的run函数你这样做:

for(long i = 0; i < 1000000000; i++) { 
    fake++; 
} 

,我不从你的问题的理解:

我使用变量假的run方法要抓紧时间“可读”。

这是什么意思。但是每个线程都需要增加你的变量1000000000次。

+0

我假设OP使用'fake'变量来填充线程的运行时间,否则它们完成的速度太快,无法以millis分辨率绘制比较结果。 –

+0

我使用假变量使运行方法持续更多,以便我可以追踪人类可读的时间。如果我删除伪变量的运行方法的持续时间太短,它给我的执行时间为0 –

0

作为一个方面说明,对于您要做的事情,有Fork/Join-Framework。它允许您递归地轻松分割任务并实现一种自动分配工作负载的算法。

有一个guide available here;它的例子非常相似,你的情况,这归结为RecursiveTask这样的:

class Adder extends RecursiveTask<Integer> 
{ 
    private int[] toAdd; 
    private int from; 
    private int to; 

    /** Add the numbers in the given array */ 
    public Adder(int[] toAdd) 
    { 
     this(toAdd, 0, toAdd.length); 
    } 

    /** Add the numbers in the given array between the given indices; 
     internal constructor to split work */ 
    private Adder(int[] toAdd, int fromIndex, int upToIndex) 
    { 
     this.toAdd = toAdd; 
     this.from = fromIndex; 
     this.to = upToIndex; 
    } 

    /** This is the work method */ 
    @Override 
    protected Integer compute() 
    { 
     int amount = to - from; 
     int result = 0; 
     if (amount < 500) 
     { 
      // base case: add ints and return the result 
      for (int i = from; i < to; i++) 
      { 
       result += toAdd[i]; 
      } 
     } 
     else 
     { 
      // array too large: split it into two parts and distribute the actual adding 
      int newEndIndex = from + (amount/2); 
      Collection<Adder> invokeAll = invokeAll(Arrays.asList(
        new Adder(toAdd, from, newEndIndex), 
        new Adder(toAdd, newEndIndex, to))); 
      for (Adder a : invokeAll) 
      { 
       result += a.invoke(); 
      } 
     } 
     return result; 
    } 
} 

要真正运行这个,你可以使用

RecursiveTask adder = new Adder(fillArray(ARRAY_LENGTH)); 
int result = ForkJoinPool.commonPool().invoke(adder);