多线程运行速度比单进程慢
MultiThread runs slower than single process
学校的一项作业要求我创建一个简单的程序来创建 1000 个文本文件,每个文件的行数都是随机的,通过 multi-thread\single 过程计算有多少行。而不是删除这些文件。
现在在测试过程中发生了一件奇怪的事情——所有文件的线性计数总是比以多线程方式对它们计数要快一点,这在我的课堂上引发了相当多的学术理论讨论。
当使用 Scanner
读取所有文件时,一切都按预期工作 - 以大约 500 毫秒的线性时间和 400 毫秒的线程时间读取 1000 个文件
然而,当我使用 BufferedReader
时,线性时间下降到大约 110 毫秒,线程时间下降到 130 毫秒。
代码的哪一部分导致了这个瓶颈,为什么?
编辑:澄清一下,我不是在问为什么 Scanner
比 BufferedReader
慢。
完整的可编译代码:(尽管您应该更改文件创建路径输出)
import java.io.*;
import java.util.Random;
import java.util.Scanner;
/**
* Builds text files with random amount of lines and counts them with
* one process or multi-threading.
* @author Hazir
*/// CLASS MATALA_4A START:
public class Matala_4A {
/* Finals: */
private static final String MSG = "Hello World";
/* Privates: */
private static int count;
private static Random rand;
/* Private Methods: */ /**
* Increases the random generator.
* @return The new random value.
*/
private static synchronized int getRand() {
return rand.nextInt(1000);
}
/**
* Increments the lines-read counter by a value.
* @param val The amount to be incremented by.
*/
private static synchronized void incrementCount(int val) {
count+=val;
}
/**
* Sets lines-read counter to 0 and Initializes random generator
* by the seed - 123.
*/
private static void Initialize() {
count=0;
rand = new Random(123);
}
/* Public Methods: */ /**
* Creates n files with random amount of lines.
* @param n The amount of files to be created.
* @return String array with all the file paths.
*/
public static String[] createFiles(int n) {
String[] array = new String[n];
for (int i=0; i<n; i++) {
array[i] = String.format("C:\Files\File_%d.txt", i+1);
try ( // Try with Resources:
FileWriter fw = new FileWriter(array[i]);
PrintWriter pw = new PrintWriter(fw);
) {
int numLines = getRand();
for (int j=0; j<numLines; j++) pw.println(MSG);
} catch (IOException ex) {
System.err.println(String.format("Failed Writing to file: %s",
array[i]));
}
}
return array;
}
/**
* Deletes all the files who's file paths are specified
* in the fileNames array.
* @param fileNames The files to be deleted.
*/
public static void deleteFiles(String[] fileNames) {
for (String fileName : fileNames) {
File file = new File(fileName);
if (file.exists()) {
file.delete();
}
}
}
/**
* Creates numFiles amount of files.<br>
* Counts how many lines are in all the files via Multi-threading.<br>
* Deletes all the files when finished.
* @param numFiles The amount of files to be created.
*/
public static void countLinesThread(int numFiles) {
Initialize();
/* Create Files */
String[] fileNames = createFiles(numFiles);
Thread[] running = new Thread[numFiles];
int k=0;
long start = System.currentTimeMillis();
/* Start all threads */
for (String fileName : fileNames) {
LineCounter thread = new LineCounter(fileName);
running[k++] = thread;
thread.start();
}
/* Join all threads */
for (Thread thread : running) {
try {
thread.join();
} catch (InterruptedException e) {
// Shouldn't happen.
}
}
long end = System.currentTimeMillis();
System.out.println(String.format("threads time = %d ms, lines = %d",
end-start,count));
/* Delete all files */
deleteFiles(fileNames);
}
@SuppressWarnings("CallToThreadRun")
/**
* Creates numFiles amount of files.<br>
* Counts how many lines are in all the files in one process.<br>
* Deletes all the files when finished.
* @param numFiles The amount of files to be created.
*/
public static void countLinesOneProcess(int numFiles) {
Initialize();
/* Create Files */
String[] fileNames = createFiles(numFiles);
/* Iterate Files*/
long start = System.currentTimeMillis();
LineCounter thread;
for (String fileName : fileNames) {
thread = new LineCounter(fileName);
thread.run(); // same process
}
long end = System.currentTimeMillis();
System.out.println(String.format("linear time = %d ms, lines = %d",
end-start,count));
/* Delete all files */
deleteFiles(fileNames);
}
public static void main(String[] args) {
int num = 1000;
countLinesThread(num);
countLinesOneProcess(num);
}
/**
* Auxiliary class designed to count the amount of lines in a text file.
*/// NESTED CLASS LINECOUNTER START:
private static class LineCounter extends Thread {
/* Privates: */
private String fileName;
/* Constructor: */
private LineCounter(String fileName) {
this.fileName=fileName;
}
/* Methods: */
/**
* Reads a file and counts the amount of lines it has.
*/ @Override
public void run() {
int count=0;
try ( // Try with Resources:
FileReader fr = new FileReader(fileName);
//Scanner sc = new Scanner(fr);
BufferedReader br = new BufferedReader(fr);
) {
String str;
for (str=br.readLine(); str!=null; str=br.readLine()) count++;
//for (; sc.hasNext(); sc.nextLine()) count++;
incrementCount(count);
} catch (IOException e) {
System.err.println(String.format("Failed Reading from file: %s",
fileName));
}
}
} // NESTED CLASS LINECOUNTER END;
} // CLASS MATALA_4A END;
瓶颈是磁盘。
您每次只能使用一个线程访问磁盘,因此使用多个线程无济于事,而且线程切换所需的超时会降低您的全局性能。
仅当您需要拆分等待不同来源(例如网络和磁盘,或两个不同的磁盘,或许多网络流)上的长时间 I/O 操作的工作时,或者如果您有cpu 密集型操作,可以在不同的核心之间拆分。
请记住,对于一个好的多线程程序,您需要始终考虑:
- 在线程之间切换上下文时间
- long I/O 操作是否可以并行完成
- 密集型cpu 是否存在计算时间
- cpu 计算是否可以拆分为子问题
- 线程间共享数据的复杂性(信号量或同步)
- 与单线程应用程序相比,多线程代码难以读取、编写和管理
使用的线程数非常重要。尝试在 1000 个线程之间切换的单个进程(您为每个文件创建了一个新线程)可能是变慢的主要原因。
尝试使用假设 10 个线程来读取 1000 个文件,然后您会看到明显的速度提升
可能有不同的因素:
最重要的是避免同时从多个线程访问磁盘(但由于您使用的是 SSD,您可能会逃脱)。然而,在普通硬盘上,从一个文件切换到另一个文件可能会花费 10 毫秒的寻道时间(取决于数据的缓存方式)。
1000个线程太多了,尽量使用核心数*2。太多时间只会浪费切换上下文。
尝试使用线程池。总时间在 110 毫秒到 130 毫秒之间,其中一部分来自创建线程。
总的来说,在测试中多做一些工作。计时 110 毫秒并不总是那么准确。也取决于当时其他进程或线程是什么运行。
尝试切换测试的顺序以查看是否有所不同(缓存可能是一个重要因素)
countLinesThread(num);
countLinesOneProcess(num);
此外,根据系统的不同,currentTimeMillis()
的分辨率可能为 10 到 15 毫秒。所以短跑的时间不是很准确。
long start = System.currentTimeMillis();
long end = System.currentTimeMillis();
如果计算所需的实际时间与 I/O 所需的时间相比可以忽略不计,则潜在的 multi-threding 好处也可以忽略不计:一个线程能够很好地使 [=16] 饱和=] 然后将进行 非常 的快速计算;更多的线程不能加快速度。相反,将应用通常的线程开销,加上 I/O 实现中可能的锁定惩罚实际上降低了吞吐量。
我认为当 CPU 处理数据块所需的时间比从磁盘获取数据块的时间长时,潜在的好处最大。在那种情况下,除了当前正在读取的线程(如果有的话)之外的所有线程都可以计算,并且执行速度应该与内核数量很好地成比例。尝试从文件中检查大素数候选者或破解加密行(这有点,相当于同一件事,够愚蠢的)。
学校的一项作业要求我创建一个简单的程序来创建 1000 个文本文件,每个文件的行数都是随机的,通过 multi-thread\single 过程计算有多少行。而不是删除这些文件。
现在在测试过程中发生了一件奇怪的事情——所有文件的线性计数总是比以多线程方式对它们计数要快一点,这在我的课堂上引发了相当多的学术理论讨论。
当使用 Scanner
读取所有文件时,一切都按预期工作 - 以大约 500 毫秒的线性时间和 400 毫秒的线程时间读取 1000 个文件
然而,当我使用 BufferedReader
时,线性时间下降到大约 110 毫秒,线程时间下降到 130 毫秒。
代码的哪一部分导致了这个瓶颈,为什么?
编辑:澄清一下,我不是在问为什么 Scanner
比 BufferedReader
慢。
完整的可编译代码:(尽管您应该更改文件创建路径输出)
import java.io.*;
import java.util.Random;
import java.util.Scanner;
/**
* Builds text files with random amount of lines and counts them with
* one process or multi-threading.
* @author Hazir
*/// CLASS MATALA_4A START:
public class Matala_4A {
/* Finals: */
private static final String MSG = "Hello World";
/* Privates: */
private static int count;
private static Random rand;
/* Private Methods: */ /**
* Increases the random generator.
* @return The new random value.
*/
private static synchronized int getRand() {
return rand.nextInt(1000);
}
/**
* Increments the lines-read counter by a value.
* @param val The amount to be incremented by.
*/
private static synchronized void incrementCount(int val) {
count+=val;
}
/**
* Sets lines-read counter to 0 and Initializes random generator
* by the seed - 123.
*/
private static void Initialize() {
count=0;
rand = new Random(123);
}
/* Public Methods: */ /**
* Creates n files with random amount of lines.
* @param n The amount of files to be created.
* @return String array with all the file paths.
*/
public static String[] createFiles(int n) {
String[] array = new String[n];
for (int i=0; i<n; i++) {
array[i] = String.format("C:\Files\File_%d.txt", i+1);
try ( // Try with Resources:
FileWriter fw = new FileWriter(array[i]);
PrintWriter pw = new PrintWriter(fw);
) {
int numLines = getRand();
for (int j=0; j<numLines; j++) pw.println(MSG);
} catch (IOException ex) {
System.err.println(String.format("Failed Writing to file: %s",
array[i]));
}
}
return array;
}
/**
* Deletes all the files who's file paths are specified
* in the fileNames array.
* @param fileNames The files to be deleted.
*/
public static void deleteFiles(String[] fileNames) {
for (String fileName : fileNames) {
File file = new File(fileName);
if (file.exists()) {
file.delete();
}
}
}
/**
* Creates numFiles amount of files.<br>
* Counts how many lines are in all the files via Multi-threading.<br>
* Deletes all the files when finished.
* @param numFiles The amount of files to be created.
*/
public static void countLinesThread(int numFiles) {
Initialize();
/* Create Files */
String[] fileNames = createFiles(numFiles);
Thread[] running = new Thread[numFiles];
int k=0;
long start = System.currentTimeMillis();
/* Start all threads */
for (String fileName : fileNames) {
LineCounter thread = new LineCounter(fileName);
running[k++] = thread;
thread.start();
}
/* Join all threads */
for (Thread thread : running) {
try {
thread.join();
} catch (InterruptedException e) {
// Shouldn't happen.
}
}
long end = System.currentTimeMillis();
System.out.println(String.format("threads time = %d ms, lines = %d",
end-start,count));
/* Delete all files */
deleteFiles(fileNames);
}
@SuppressWarnings("CallToThreadRun")
/**
* Creates numFiles amount of files.<br>
* Counts how many lines are in all the files in one process.<br>
* Deletes all the files when finished.
* @param numFiles The amount of files to be created.
*/
public static void countLinesOneProcess(int numFiles) {
Initialize();
/* Create Files */
String[] fileNames = createFiles(numFiles);
/* Iterate Files*/
long start = System.currentTimeMillis();
LineCounter thread;
for (String fileName : fileNames) {
thread = new LineCounter(fileName);
thread.run(); // same process
}
long end = System.currentTimeMillis();
System.out.println(String.format("linear time = %d ms, lines = %d",
end-start,count));
/* Delete all files */
deleteFiles(fileNames);
}
public static void main(String[] args) {
int num = 1000;
countLinesThread(num);
countLinesOneProcess(num);
}
/**
* Auxiliary class designed to count the amount of lines in a text file.
*/// NESTED CLASS LINECOUNTER START:
private static class LineCounter extends Thread {
/* Privates: */
private String fileName;
/* Constructor: */
private LineCounter(String fileName) {
this.fileName=fileName;
}
/* Methods: */
/**
* Reads a file and counts the amount of lines it has.
*/ @Override
public void run() {
int count=0;
try ( // Try with Resources:
FileReader fr = new FileReader(fileName);
//Scanner sc = new Scanner(fr);
BufferedReader br = new BufferedReader(fr);
) {
String str;
for (str=br.readLine(); str!=null; str=br.readLine()) count++;
//for (; sc.hasNext(); sc.nextLine()) count++;
incrementCount(count);
} catch (IOException e) {
System.err.println(String.format("Failed Reading from file: %s",
fileName));
}
}
} // NESTED CLASS LINECOUNTER END;
} // CLASS MATALA_4A END;
瓶颈是磁盘。
您每次只能使用一个线程访问磁盘,因此使用多个线程无济于事,而且线程切换所需的超时会降低您的全局性能。
仅当您需要拆分等待不同来源(例如网络和磁盘,或两个不同的磁盘,或许多网络流)上的长时间 I/O 操作的工作时,或者如果您有cpu 密集型操作,可以在不同的核心之间拆分。
请记住,对于一个好的多线程程序,您需要始终考虑:
- 在线程之间切换上下文时间
- long I/O 操作是否可以并行完成
- 密集型cpu 是否存在计算时间
- cpu 计算是否可以拆分为子问题
- 线程间共享数据的复杂性(信号量或同步)
- 与单线程应用程序相比,多线程代码难以读取、编写和管理
使用的线程数非常重要。尝试在 1000 个线程之间切换的单个进程(您为每个文件创建了一个新线程)可能是变慢的主要原因。
尝试使用假设 10 个线程来读取 1000 个文件,然后您会看到明显的速度提升
可能有不同的因素:
最重要的是避免同时从多个线程访问磁盘(但由于您使用的是 SSD,您可能会逃脱)。然而,在普通硬盘上,从一个文件切换到另一个文件可能会花费 10 毫秒的寻道时间(取决于数据的缓存方式)。
1000个线程太多了,尽量使用核心数*2。太多时间只会浪费切换上下文。
尝试使用线程池。总时间在 110 毫秒到 130 毫秒之间,其中一部分来自创建线程。
总的来说,在测试中多做一些工作。计时 110 毫秒并不总是那么准确。也取决于当时其他进程或线程是什么运行。
尝试切换测试的顺序以查看是否有所不同(缓存可能是一个重要因素)
countLinesThread(num); countLinesOneProcess(num);
此外,根据系统的不同,currentTimeMillis()
的分辨率可能为 10 到 15 毫秒。所以短跑的时间不是很准确。
long start = System.currentTimeMillis();
long end = System.currentTimeMillis();
如果计算所需的实际时间与 I/O 所需的时间相比可以忽略不计,则潜在的 multi-threding 好处也可以忽略不计:一个线程能够很好地使 [=16] 饱和=] 然后将进行 非常 的快速计算;更多的线程不能加快速度。相反,将应用通常的线程开销,加上 I/O 实现中可能的锁定惩罚实际上降低了吞吐量。
我认为当 CPU 处理数据块所需的时间比从磁盘获取数据块的时间长时,潜在的好处最大。在那种情况下,除了当前正在读取的线程(如果有的话)之外的所有线程都可以计算,并且执行速度应该与内核数量很好地成比例。尝试从文件中检查大素数候选者或破解加密行(这有点,相当于同一件事,够愚蠢的)。