当有很多个文件需要进行处理的时候,我们为了提高程序执行的性能,往往想当然的开多个线程并行执行文件的读/写动作。但是其实这种“想当然”是错误的,下面我们就来看看,对于磁盘IO密集型的应用,多线程到底带来了什么?
首先,我写了一段读文件的程序,这个程序支持用单线程/多线程两种方式读入多个文件,并且记录整个读文件的耗时,最后来比较一下单线程/多线程两种模型在读文件上的性能差别:
- public class TestMultiThreadIO {
- /**
- * @param args
- * @throws IOException
- */
- public static void main(String[] args) throws Exception {
- if (args.length != 2) {
- throw new IllegalArgumentException("Usage: isSingle[true|false] filenames(split with ,)");
- }
- long startTime = System.currentTimeMillis();
- boolean isSingle = Boolean.parseBoolean(args[0]);
- String[] filenames = args[1].split(",");
- // 主线程先打开这组文件,为了排除掉单线程与多线程打开文件的性能差异,关注点是读文件的过程
- InputStream[] inputFiles = new InputStream[filenames.length];
- for (int i = 0; i < inputFiles.length; i++) {
- inputFiles[i] = new BufferedInputStream(new FileInputStream(filenames[i]));
- }
- if (isSingle) {
- System.out.println("single thread cost: " + singleThread(inputFiles) + " ms");
- } else {
- System.out.println("multi thread cost: " + multiThread(inputFiles) + " ms");
- }
- for (int i = 0; i < inputFiles.length; i++) {
- inputFiles[i].close();
- }
- System.out.println("finished, total cost: " + (System.currentTimeMillis() - startTime));
- }
- private static long singleThread(InputStream[] inputFiles) throws IOException {
- long start = System.currentTimeMillis();
- for (InputStream in : inputFiles) {
- while (in.read() != -1) {
- }
- }
- return System.currentTimeMillis() - start;
- }
- private static long multiThread(final InputStream[] inputFiles) throws Exception {
- int threadCount = inputFiles.length;
- final CyclicBarrier barrier = new CyclicBarrier(threadCount + 1);
- final CountDownLatch latch = new CountDownLatch(threadCount);
- for (final InputStream in : inputFiles) {
- Thread t = new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- barrier.await();
- while (in.read() != -1) {
- }
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- latch.countDown();
- }
- }
- });
- t.start();
- }
- long start = System.currentTimeMillis();
- barrier.await();
- latch.await();
- return (System.currentTimeMillis() - start);
- }
- }
程序写好了,下面介绍一下我的测试环境:
CPU: 24核(Intel(R) Xeon(R) CPU E5-2620 0 @ 2.00GHz)
内存:32GB
系统:64位 CentOS release 5.8
在测试之前,需要说明一下,Linux系统为了提高IO性能,对于文件的读写会由操作系统缓存起来,这就是cached的作用:
- $ free -m
- total used free shared buffers cached
- Mem: 32144 818 31325 0 0 8
- -/+ buffers/cache: 809 31334
- Swap: 4096 38 4057
这里我先准备好了一个文件,文件名叫“0”,我们下面尝试读入这个文件:
- $ dd if=0 of=/dev/null bs=1024b count=100
再用free -m看,可以看到cached空间增长了50MB:
- $ free -m
- total used free shared buffers cached
- Mem: 32144 868 31276 0 0 58
- -/+ buffers/cache: 808 31335
- Swap: 4096 38 4057
所以,我们在测试时,为了排除系统缓存对测试的影响,应该在每次测试完成后都主动将系统缓存清空:
- sync && echo 3 > /proc/sys/vm/drop_caches && echo 0 > /proc/sys/vm/drop_caches
清空后可以看到cached确实还原了:
- $ free -m
- total used free shared buffers cached
- Mem: 32144 818 31326 0 0 8
- -/+ buffers/cache: 809 31334
- Swap: 4096 38 4057
具体有关cached/buffers的信息有兴趣的同学可以google一下。
下面开始正式测试
测试用例1:
先生成一批文件,为了方便,文件名都按照0、1、2...的序号来命名,每个文件内容不同(用dd+urandom生成),大小相同都是50MB的文件。
然后开始进行单线程读取10个文件的测试:
- $ java TestMultiThreadIO true 0,1,2,3,4,5,6,7,8,9 && sync && echo 3 > /proc/sys/vm/drop_caches && echo 0 > /proc/sys/vm/drop_caches
- single thread cost: 20312 ms
- finished, total cost: 20322
下面是测试进行过程中,系统的各项资源开销:
- ----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
- usr sys idl wai hiq siq| read writ| recv send| in out | int csw
- 0 0 98 2 0 0| 17M 0 | 384B 412B| 0 0 |1139 374
- 2 0 96 2 0 0| 38M 0 | 686B 522B| 0 0 |1502 1083
- 4 0 96 0 0 0| 51M 0 | 448B 412B| 0 0 |1223 194
- 4 0 96 0 0 0| 51M 0 | 448B 412B| 0 0 |1220 186
- 4 0 95 0 0 0| 50M 0 | 812B 412B| 0 0 |1215 235
- 4 0 96 0 0 0| 48M 240k| 448B 412B| 0 0 |1218 286
- 4 0 96 0 0 0| 49M 24k| 512B 412B| 0 0 |1213 213
- 4 0 96 0 0 0| 50M 0 | 622B 476B| 0 0 |1218 200
- 4 0 96 0 0 0| 51M 0 | 384B 412B| 0 0 |1215 188
- 4 0 96 0 0 0| 51M 0 | 448B 412B| 0 0 |1219 174
- 4 0 94 2 0 0| 44M 0 | 448B 412B| 0 0 |1194 339
- 4 0 94 2 0 0| 49M 24k| 862B 412B| 0 0 |1219 366
- 4 0 96 0 0 0| 51M 0 | 384B 886B| 0 0 |1215 194
- 4 0 96 0 0 0| 50M 0 | 512B 1112B| 0 0 |1218 194
- 4 0 96 0 0 0| 51M 0 | 448B 412B| 0 0 |1216 184
- 4 0 95 1 0 0| 49M 48k| 558B 540B| 0 0 |1216 298
- 4 0 96 0 0 0| 50M 24k| 384B 412B| 0 0 |1215 272
- 4 0 96 0 0 0| 50M 48k| 448B 412B| 0 0 |1220 236
- 4 0 96 0 0 0| 50M 168k| 448B 412B| 0 0 |1225 187
- 4 0 96 0 0 0| 51M 0 | 448B 476B| 0 0 |1217 178
- 4 0 96 1 0 0| 44M 0 | 384B 412B| 0 0 |1187 180
- 3 0 96 1 0 0| 38M 240k| 448B 412B| 0 0 |1208 315
小结:程序总共耗时20s,由于是单线程读,所以cpu开销非常小,usr在4%,基本没有iowait。上下文切换开销在200~300左右。
测试用例2:
同样是这10个文件,用多线程读取(每个文件一个线程):
- $ java TestMultiThreadIO false 0,1,2,3,4,5,6,7,8,9 && sync && echo 3 > /proc/sys/vm/drop_caches && echo 0 > /proc/sys/vm/drop_caches
- multi thread cost: 19124 ms
- finished, total cost: 19144
下面是测试进行过程中,系统的各项资源开销:
- ----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
- usr sys idl wai hiq siq| read writ| recv send| in out | int csw
- 1 0 95 4 0 0| 29M 0 | 512B 412B| 0 0 |1515 1402
- 5 0 75 20 0 0| 54M 0 | 622B 428B| 0 0 |1253 652
- 4 0 74 21 0 0| 56M 0 | 320B 318B| 0 0 |1245 601
- 5 0 76 20 0 0| 56M 0 | 448B 318B| 0 0 |1242 602
- 4 0 84 12 0 0| 52M 0 | 622B 476B| 0 0 |1228 600
- 4 0 85 12 0 0| 45M 264k| 384B 412B| 0 0 |1201 539
- 4 0 83 12 0 0| 53M 0 | 798B 886B| 0 0 |1225 588
- 4 0 81 15 0 0| 54M 0 | 384B 412B| 0 0 |1234 596
- 4 0 86 10 0 0| 52M 16k| 384B 412B| 0 0 |1233 597
- 4 0 86 9 0 0| 53M 0 | 448B 412B| 0 0 |1237 582
- 4 0 80 16 0 0| 53M 0 | 896B 412B| 0 0 |1227 593
- 4 0 85 10 0 0| 52M 88k| 448B 412B| 0 0 |1238 607
- 4 0 83 13 0 0| 54M 0 | 384B 412B| 0 0 |1236 607
- 4 0 75 20 0 0| 55M 0 | 384B 412B| 0 0 |1238 587
- 4 0 80 15 0 0| 54M 0 | 448B 412B| 0 0 |1236 588
- 4 0 83 13 0 0| 46M 0 | 558B 476B| 0 0 |1200 528
- 4 0 77 19 0 0| 51M 16k| 448B 412B| 0 0 |1227 605
- 4 0 82 14 0 0| 52M 8192B| 448B 412B| 0 0 |1227 571
- 4 0 88 7 0 0| 56M 0 | 448B 412B| 0 0 |1245 615
- 4 0 94 2 0 0| 54M 0 | 384B 412B| 0 0 |1231 480
- 0 0 99 1 0 0|1056k 584k| 512B 884B| 0 0 |1092 235
由于wai比较高,所以再来看一下iostat的状况(iostat命令详解参考这里):
- iostat -x 1
- Device: rrqm/s wrqm/s r/s w/s rsec/s wsec/s avgrq-sz avgqu-sz await svctm %util
- sda 103.00 2.00 228.00 0.00 54088.00 0.00 237.23 9.28 62.40 4.39 100.10
- sda1 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
- sda2 0.00 2.00 0.00 0.00 0.00 0.00 0.00 0.10 0.00 0.00 9.50
- sda3 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
- sda4 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
- sda5 103.00 0.00 228.00 0.00 54088.00 0.00 237.23 9.18 62.40 4.39 100.10
- avg-cpu: %user %nice %system %iowait %steal %idle
- 3.67 0.00 0.00 5.29 0.00 91.05
小结:程序总耗时19s,usr占用依然在4%(由于我们的测试程序基本没有什么计算工作,只是简单的读文件)。但通过iostat看到util已经到达100%,每次IO等待时间达到62ms,上下文开销也增长到500~600。
可以看到,1个线程增加到10个线程,执行时间仅仅降低了1s,但系统开销大了很多,主要是阻塞在IO操作上。
如果再加大线程数会发生什么呢?下面是用10/20/50/100个线程测试的结果:
总结:可以看出,当测试文件增多时,在单线程情况下,性能没有降低。但多线程情况下,性能降低的很明显,由于IO阻塞导致CPU基本被吃满。所以在实际编码过程中,如果遇到文件读写操作,最好用一个单独的线程做,其它线程可以分配给计算/网络IO等其它地方。而且要注意多个进程之间的文件IO的影响,如果多个进程分别做顺序IO,其实全局来看(如果是一块磁盘),就变成了随机IO,也会影响系统性能。
http://blueswind8306.iteye.com/blog/1983914