2017-10-04 88 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);