与Java中的多处理相比,多线程性能较差



假设我们有数百万行必须解析的文本
在我的i7 2600 CPU上,解析每1000行大约需要13毫秒
因此,解析1000000行大约需要13秒
为了减少执行时间,我使用了多个线程
使用阻塞队列,我将1000000行作为一组1000个区块推送,每个区块包含1000行,并使用8个线程消耗这些区块。代码很简单,似乎可以工作,但性能并不令人鼓舞,大约需要11秒
以下是多线程代码的主要部分:

for(int i=0;i<threadCount;i++)
{
Runnable r=new Runnable() {
public void run() {
try{
while (true){
InputType chunk=inputQ.poll(10, TimeUnit.MILLISECONDS);
if(chunk==null){
if(inputRemains.get())
continue;
else
return;
}
processItem(chunk);
}
}catch (Exception e) {
e.printStackTrace();  
}
}
};
Thread t=new Thread(r);
threadList.add(t);
for(Thread t: threads)
t.join();

我也使用过ExecutorService,但性能更差
更改区块大小也没有帮助,性能也没有改善
这意味着阻塞队列不是瓶颈
另一方面,当我同时运行串行程序的4个实例时,只需15秒即可完成所有4个实例。这意味着我可以在15秒内使用4个进程处理40000000行,因此,与1.2的多线程速度相比,速度提高了3.4左右,这是非常有希望的

我想知道有没有人知道这件事
问题很直接:阻塞队列中的一组行和几个线程从队列中轮询项目并并行处理它们。队列最初已填充,因此线程处于完全繁忙状态
我以前也有过类似的经历,但我不明白为什么多处理更好
我还应该提到,我在Windows7上运行测试,并使用1.7 JRE
任何想法都是受欢迎的,并表示感谢。

编辑:

所以我最初认为你的时间安排是围绕着你的整个项目。如果只是在将行读取到内存后对其进行处理,则可能是您的processItem(chunk);方法正在执行自己的IO,或者正在将信息写入synchronized对象或其他共享变量,从而使其无法同时完整运行。


我想知道有人知道这件事吗?

您的问题可能是您绑定了IO,而不是CPU板。通过添加更多的线程,您将获得速度大幅提高的唯一方法是,如果您执行的CPU处理比从磁盘读取(或写入)磁盘的CPU处理多。一旦您已经将磁盘子系统的IO功能发挥到了极致,就没有什么可以提高处理速度的了。正如您所展示的,添加更多的线程实际上会减慢绑定到IO的程序的速度。

我会添加一个额外的线程(即2个处理线程),看看这是否有帮助。如果你所得到的只是2秒的速度提高,那么你将不得不将文件分割到多个驱动器上,或者如果这是一项重复的任务,则必须将其移动到内存驱动器上才能更快地读取。

我也使用过ExecutorService,但性能更差!

这可能是因为您在每次迭代/块中使用了太多线程,或者处理的行太少。

另一方面,当我同时运行串行程序的4个实例时,只需要15秒就可以完成所有4个实例的

我怀疑这是因为他们每个人都可以从操作系统使用彼此的磁盘缓存。当第一个应用程序读取块#1时,其他3个应用程序不必这样做。尝试复制文件4次,并尝试4个串行应用程序同时运行,每个应用程序都在自己的文件上。你应该看到区别。

我会责怪你的代码的并行化。如果项目可供处理,那么多个线程将竞争同一资源(队列)。对同步锁的争夺有点像性能杀手。如果项目的处理速度快于它们被添加到队列的速度,那么被饿死的线程几乎只是繁忙的循环,例如while (true) {}。这是因为您的轮询时间很短,当轮询失败时,您只需立即重试。

关于同步的小提示。首先,JVM使用繁忙循环来等待资源变得可用,因为(通常)编写代码是为了尽快释放同步锁,而另一种选择(进行上下文切换)非常昂贵。最终,如果JVM发现它大部分时间都在等待同步锁,那么如果它无法获得锁,它将默认切换到另一个线程。

一个更好的解决方案是让一个线程读取数据,并在线程有可用插槽和新线程有数据时调度一个新线程。这里的Executor非常有用,因为它可以跟踪哪些线程已经完成,哪些线程仍然繁忙。但伪代码看起来像:

int charsRead;
char[] buffer = new char[BUF_SIZE];
int startIndex = 0;
while((charsRead = inputStreamReader.read(buffer, startIndex, buffer.length)
!= -1) {
// find last new line so don't give a thread any partial lines
int lastNewLine = findFirstNewLineBeforeIndex(buffer, charsRead);
waitForAvailableThread(); // if not max threads running then should return 
// immediately
Thread t = new Thread(createRunnable(buffer, lastNewLine));
t.start();
addRunningThread(t);
// copy any overshoot to the start of a new buffer
// use a new buffer as the another thread is now reading from the previous 
// buffer
char[] newBuffer = new char[BUF_SIZE];
System.arraycopy(buffer, lastNewLine+1, newBuffer, 0, 
charsRead-lastNewLine-1);
buffer = newBuffer;
}
waitForRemainingThreadsToTerminate();

解析每1000行大约需要13毫秒。因此,解析1000000行大约需要13秒。

jVM在完成10000次之后才会预热,之后速度可能会快10-100倍,因此可能是13秒,也可能是130毫秒或更短。

使用阻塞队列,我将1000000行推送为一组1000个区块,每个区块包含1000行,并使用8个线程消耗这些区块。代码很简单,似乎可以工作,但性能并不令人鼓舞,大约需要11秒。

我建议你重新测试一个线程,你可能会发现它需要不到11秒的时间。

瓶颈是将String解析成一行并创建String对象所需的时间,其余的只是开销,不能解决真正的瓶颈问题。


如果你读取不同的文件,每个cpu一个,你可以接近线性速度。读取行的问题是,您必须一个接一个地读取,并且您从并发性中获得的好处很小。

2600正在为8个线程使用HT(超线程)。。解析主要是内存工作,因此从HT中获益甚微。

最新更新