为什么多线程放慢了速度

时间:2016-02-08 07:44:11

标签: java multithreading javax.imageio

我试图将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,请告诉我。

编辑显示图片被保存为不同的图像,而不是相互覆盖。

3 个答案:

答案 0 :(得分:5)

我认为这是由不同类型的优化引起的。您试图在一个路径中一次保存多个图像 - 这意味着需要 排队保存操作 - 这是一个IO绑定任务,不受CPU限制。多个保存线程在这里可能不是很有帮助。同样在非常小的(就CPU功率要求而言)操作中,委托线程来完成工作可能只会产生额外的开销,从而导致完成任务所需的时间延长,而不是缩短。希望这有帮助:)

答案 1 :(得分:3)

让我们说你的持久存储(硬盘,usb stick,ssd)以50MB / s的速度写入。如果你写50MB,那么无论线程/核心数是多少,它总是需要1秒。这被称为带宽瓶颈。

实际上还会有其他瓶颈。内存,CPU或最常见的寻道时间。寻求给定块的硬盘需要几毫秒。同时写入多个文件将导致更多搜索,从而可能减慢所有写入速度。 (大)缓冲区可能有帮助。

答案 2 :(得分: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);

应用该更改后的执行时间为

duration in sequence: 607
duration in threads: 134