2016-02-08 35 views
3

我想RGB值的字節數組保存爲png圖片如下:爲什麼多線程放緩

byte[] imgArray = ...; 
int canvasSize = 512; 

ColorModel c = new ComponentColorModel(ColorSpace.getInstance(ColorSpace.CS_GRAY), null, false, false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); 

Image image = Toolkit.getDefaultToolkit().createImage(
      new MemoryImageSource(canvasSize, canvasSize, c, imgArray, 0, canvasSize)); 

BufferedImage bimage = new BufferedImage(canvasSize, canvasSize, BufferedImage.TYPE_BYTE_GRAY); 

// Draw the image on to the buffered image 
Graphics2D bGr = bimage.createGraphics(); 
bGr.drawImage(image, 0, 0, null); //This is what takes all the time 
bGr.dispose(); 

ImageIO.write(bimage, "PNG", new File(uniqueFileName)); 

我使用FixedThreadpool同時保存多個圖像。我使用的線程越多(達到計算機上的免費核心數量),保存過程就會越長。在6個線程上運行幾乎是在一個線程上運行的兩倍。

爲什麼這需要更多的多線程?內存交換?我可以避免這個問題嗎?

另外,如果有更好的方法來保存數組中的png,請告訴我。

編輯顯示照片被保存爲不同的圖像,而不是相互覆蓋。

+0

您的機器有多少個物理處理器內核? – RAnders00

+0

8,已經有兩個正在使用。 – Nate

+1

你提到'bGr.darwImage'需要所有的時間?所以你的意思是不寫入磁盤會減慢速度?你是如何衡量這一點的?你能顯示線程部分的代碼嗎?由於圖像尺寸較小,尺寸爲512x512像素,因此繪製/寫入磁盤不應成爲瓶頸。 (現在已經刪除了與BufferedOutputStream相關的答案)。 – SubOptimal

回答

2

最初我還以爲主要原因可能是concurrent write操作。由於寫入量小於2 MB,磁盤I/O通常應該沒有瓶頸。經過一番調查後,我找到了原因。在你的情況下,ImageIO正在使用一種同步的方法(sun.java2d.cmm.lcms.LCMSTransform.doTransform)。

我用這個小代碼來確認你發現的行爲並找到鎖定條件。

package sub.optimal.jai; 

import java.awt.Image; 
import java.awt.Toolkit; 
import java.awt.Transparency; 
import java.awt.color.ColorSpace; 
import java.awt.image.BufferedImage; 
import java.awt.image.ColorModel; 
import java.awt.image.ComponentColorModel; 
import java.awt.image.DataBuffer; 
import java.awt.image.MemoryImageSource; 
import java.io.File; 
import java.io.IOException; 
import java.util.ArrayList; 
import java.util.List; 
import java.util.concurrent.Callable; 
import java.util.concurrent.ExecutorService; 
import java.util.concurrent.Executors; 
import javax.imageio.ImageIO; 

public class ThreadedOutput implements Callable<Boolean> { 

    private final String fileName; 

    private ThreadedOutput(String name) { 
     this.fileName = name; 
    } 

    @Override 
    public Boolean call() throws Exception { 
     Thread.currentThread().setName("convert: " + fileName); 
     return this.storeImage(); 
    } 

    public boolean storeImage() throws IOException { 
     byte[] imgArray = new byte[512 * 512]; 
     int canvasSize = 512; 
     int value = 0; 
     for (int i = 0; i < imgArray.length; i++) { 
      imgArray[i] = (byte) value; 
      value = (++value & 0xFF); 
     } 

     ColorModel colorModel = new ComponentColorModel(
       ColorSpace.getInstance(ColorSpace.CS_GRAY), null, false, 
       false, Transparency.OPAQUE, DataBuffer.TYPE_BYTE); 

     Image image = Toolkit.getDefaultToolkit().createImage(
       new MemoryImageSource(canvasSize, canvasSize, colorModel, 
         imgArray, 0, canvasSize) 
     ); 

     BufferedImage bimage = new BufferedImage(canvasSize, canvasSize, 
       BufferedImage.TYPE_BYTE_GRAY); 

     Graphics2D bGr = bimage.createGraphics(); 
     bGr.setPaintMode(); 

     System.out.printf("start %s%n", fileName); 
     long start = System.currentTimeMillis(); 
     bGr.drawImage(image, 0, 0, null); 

     long end = System.currentTimeMillis(); 
     System.out.printf("duration drawimage: %s %d%n", fileName, end-start); 
     bGr.dispose(); 

     return ImageIO.write(bimage, "PNG", new File("/tmp/" + fileName)); 
    } 

    public static void main(String[] args) throws Exception { 
     System.out.println("CPUs: " + Runtime.getRuntime() 
       .availableProcessors()); 
     ExecutorService executor = Executors.newFixedThreadPool(8); 
     List<ThreadedOutput> callables = new ArrayList<>(); 
     callables.add(new ThreadedOutput("file1.png")); 
     callables.add(new ThreadedOutput("file2.png")); 
     callables.add(new ThreadedOutput("file3.png")); 
     callables.add(new ThreadedOutput("file4.png")); 
     callables.add(new ThreadedOutput("file5.png")); 
     callables.add(new ThreadedOutput("file6.png")); 
     callables.add(new ThreadedOutput("file7.png")); 
     callables.add(new ThreadedOutput("file8.png")); 

     System.out.println("execute creation in sequence"); 
     long start = System.currentTimeMillis(); 
     for (ThreadedOutput callable : callables) { 
      callable.call(); 
     } 
     long end = System.currentTimeMillis(); 
     System.out.printf("duration in sequence: %d%n", end - start); 

     System.out.println("execute creation in parallel"); 
     start = System.currentTimeMillis(); 
     executor.invokeAll(callables); 
     executor.shutdown(); 
     end = System.currentTimeMillis(); 
     System.out.printf("duration in threads: %d%n", end - start); 
    } 
} 

執行以下示例輸出

CPUs: 4 
execute creation in sequence 
start file1.png 
duration drawimage: file1.png 1021 
start file2.png 
duration drawimage: file2.png 1021 
start file3.png 
duration drawimage: file3.png 1230 
start file4.png 
duration drawimage: file4.png 1056 
start file5.png 
duration drawimage: file5.png 1046 
start file6.png 
duration drawimage: file6.png 835 
start file7.png 
duration drawimage: file7.png 983 
start file8.png 
duration drawimage: file8.png 952 
duration in sequence: 8549 
execute creation in parallel 
start file1.png 
start file4.png 
start file2.png 
start file3.png 
start file6.png 
start file8.png 
start file5.png 
start file7.png 
duration drawimage: file6.png 18889 
duration drawimage: file1.png 19147 
duration drawimage: file8.png 19204 
duration drawimage: file5.png 19353 
duration drawimage: file7.png 19435 
duration drawimage: file3.png 19498 
duration drawimage: file2.png 19582 
duration drawimage: file4.png 19591 
duration in threads: 19612 

在八個並行線程運行創建所產生的代碼是顯着更慢。

在創建過程中的一個線程轉儲(與jstack $pid_of_example)行execute creation in parallel打印,你會發現類似的線

"convert: file1.png" #13 prio=5 os_prio=0 tid=... 
    java.lang.Thread.State: RUNNABLE 
    at sun.java2d.cmm.lcms.LCMS.colorConvert(Native Method) 
    at sun.java2d.cmm.lcms.LCMSTransform.doTransform(LCMSTransform.java:161) 
    - locked <0x00000000c463a080> (a sun.java2d.cmm.lcms.LCMSTransform) 

"convert: file2.png" #14 prio=5 os_prio=0 tid=... 
    java.lang.Thread.State: BLOCKED (on object monitor) 
    at sun.java2d.cmm.lcms.LCMSTransform.doTransform(LCMSTransform.java:140) 
    - waiting to lock <0x00000000c463a080> (a sun.java2d.cmm.lcms.LCMSTransform) 

形成線程轉儲,你可以看到線程#13保持一個鎖後顯示器locked <0x00000000c463a080>和線程#14正在等待鎖定此顯示器waiting to lock <0x00000000c463a080>

如果陣列imgArray只要你想它被寫入到圖像文件已經保存灰度信息,比你可以直接寫入數據(如已被haraldk提及。

而不是繪製圖像到另一個圖像

bGr.drawImage(image, 0, 0, null); 

你直接寫光柵圖像信息

WritableRaster raster = bimage.getRaster(); 
raster.setDataElements(0, 0, canvasSize, canvasSize, imgArray); 

執行ŧ imes在應用該更改後是

duration in sequence: 607 
duration in threads: 134 
+0

趕快找到原因吧!目前,有可能將「舊」柯達CMS取回,因爲它沒有我相信的線程問題(參見例如[IDS解決方案博客](https://blog.idrsolutions.com/2014/04/color -performance-change-newer-java-releases /)瞭解詳情)。希望這一點能在不久的將來得到解決。儘管如此,直接字節複製應該更快。 :-) – haraldK

+0

PS:你發現的這個線程問題與博客中提到的不同,你應該考慮爲此提交一個錯誤。 – haraldK

+0

@haraldK感謝您的鏈接我會做一些更多的調查,並在情況下,我會添加一個鏈接到bugreport。 – SubOptimal

5

我認爲這是由不同類型的優化引起的。您正嘗試在一條路徑中一次保存多個圖像 - 這意味着排隊保存操作的要求爲 - 這是一個IO綁定任務,不受CPU限制。多個保存線程在這裏可能不會很有幫助。同樣在非常小的(根據CPU功率要求而言)操作中,委託線程執行這項工作可能只會帶來額外的開銷,從而延長完成任務所需的時間,而不是縮短。希望這有助於:)

+1

是的,硬盤可能在混亂的順序:) – ZhongYu

+0

我知道我在複製我的代碼時犯了一個錯誤。實際上,每個圖像都不會寫入同一個文件。它們將被保存在相同的目錄中,但是作爲單獨的圖像。 – Nate

+1

@Nate - 如果你有一臺打印機,並且你有6個線程同時打印不同的頁面,你會讓打印機突然變快6倍嗎?它本質上是一個串行設備。所以是一個硬盤。主記憶也是如此...... – ZhongYu

3

可以說你的持久性存儲(硬盤,USB棒,ssd)以50MB/s寫入。如果你寫50MB,那麼無論線程/內核的數量如何,它總是需要1秒。這被稱爲帶寬瓶頸。

實際上也會有其他的瓶頸。內存,CPU或最常見的尋找時間。硬盤需要幾毫秒才能找到給定的塊。同時寫入多個文件會導致更多的查找,從而可能會減慢所有寫入。 (大)緩衝區可能會幫助那裏。