并发编程系列-多线程IO vs 单线程IO

当有很多个文件需要进行处理的时候,我们为了提高程序执行的性能,往往想当然的开多个线程并行执行文件的读/写动作。但是其实这种“想当然”是错误的,下面我们就来看看,对于磁盘IO密集型的应用,多线程到底带来了什么?

首先,我写了一段读文件的程序,这个程序支持用单线程/多线程两种方式读入多个文件,并且记录整个读文件的耗时,最后来比较一下单线程/多线程两种模型在读文件上的性能差别:

Java代码   收藏代码
  1. public class TestMultiThreadIO {  
  2.   
  3.     /** 
  4.      * @param args 
  5.      * @throws IOException  
  6.      */  
  7.     public static void main(String[] args) throws Exception {  
  8.         if (args.length != 2) {  
  9.             throw new IllegalArgumentException("Usage: isSingle[true|false] filenames(split with ,)");  
  10.         }  
  11.           
  12.         long startTime = System.currentTimeMillis();  
  13.           
  14.         boolean isSingle = Boolean.parseBoolean(args[0]);  
  15.         String[] filenames = args[1].split(",");  
  16.         // 主线程先打开这组文件,为了排除掉单线程与多线程打开文件的性能差异,关注点是读文件的过程  
  17.         InputStream[] inputFiles = new InputStream[filenames.length];  
  18.         for (int i = 0; i < inputFiles.length; i++) {  
  19.             inputFiles[i] = new BufferedInputStream(new FileInputStream(filenames[i]));  
  20.         }  
  21.           
  22.         if (isSingle) {  
  23.             System.out.println("single thread cost: " + singleThread(inputFiles) + " ms");  
  24.         } else {  
  25.             System.out.println("multi thread cost: " + multiThread(inputFiles) + " ms");  
  26.         }  
  27.           
  28.         for (int i = 0; i < inputFiles.length; i++) {  
  29.             inputFiles[i].close();  
  30.         }  
  31.           
  32.         System.out.println("finished, total cost: " + (System.currentTimeMillis() - startTime));  
  33.     }  
  34.       
  35.     private static long singleThread(InputStream[] inputFiles) throws IOException {  
  36.         long start = System.currentTimeMillis();  
  37.         for (InputStream in : inputFiles) {  
  38.             while (in.read() != -1) {  
  39.             }  
  40.         }  
  41.         return System.currentTimeMillis() - start;  
  42.     }  
  43.       
  44.     private static long multiThread(final InputStream[] inputFiles) throws Exception {  
  45.         int threadCount = inputFiles.length;  
  46.         final CyclicBarrier barrier = new CyclicBarrier(threadCount + 1);  
  47.         final CountDownLatch latch = new CountDownLatch(threadCount);  
  48.         for (final InputStream in : inputFiles) {  
  49.             Thread t = new Thread(new Runnable() {  
  50.                 @Override  
  51.                 public void run() {  
  52.                     try {  
  53.                         barrier.await();  
  54.                         while (in.read() != -1) {  
  55.                         }  
  56.                     } catch (Exception e) {  
  57.                         e.printStackTrace();  
  58.                     } finally {  
  59.                         latch.countDown();  
  60.                     }  
  61.                 }  
  62.             });  
  63.             t.start();  
  64.         }  
  65.           
  66.         long start = System.currentTimeMillis();  
  67.         barrier.await();  
  68.         latch.await();  
  69.         return (System.currentTimeMillis() - start);  
  70.     }  
  71.   
  72. }  



程序写好了,下面介绍一下我的测试环境:
CPU: 24核(Intel(R) Xeon(R) CPU E5-2620 0 @ 2.00GHz)
内存:32GB
系统:64位 CentOS release 5.8

在测试之前,需要说明一下,Linux系统为了提高IO性能,对于文件的读写会由操作系统缓存起来,这就是cached的作用:

Java代码   收藏代码
  1. $ free -m  
  2.              total       used       free     shared    buffers     cached  
  3. Mem:         32144        818      31325          0          0          8  
  4. -/+ buffers/cache:        809      31334  
  5. Swap:         4096         38       4057  


这里我先准备好了一个文件,文件名叫“0”,我们下面尝试读入这个文件:

Java代码   收藏代码
  1. $ dd if=0 of=/dev/null bs=1024b count=100  


再用free -m看,可以看到cached空间增长了50MB:

Java代码   收藏代码
  1. $ free -m  
  2.              total       used       free     shared    buffers     cached  
  3. Mem:         32144        868      31276          0          0         58  
  4. -/+ buffers/cache:        808      31335  
  5. Swap:         4096         38       4057  


所以,我们在测试时,为了排除系统缓存对测试的影响,应该在每次测试完成后都主动将系统缓存清空:

Java代码   收藏代码
  1. sync && echo 3 > /proc/sys/vm/drop_caches && echo 0 > /proc/sys/vm/drop_caches  


清空后可以看到cached确实还原了:

Java代码   收藏代码
  1. $ free -m  
  2.              total       used       free     shared    buffers     cached  
  3. Mem:         32144        818      31326          0          0          8  
  4. -/+ buffers/cache:        809      31334  
  5. Swap:         4096         38       4057  


具体有关cached/buffers的信息有兴趣的同学可以google一下。

下面开始正式测试
测试用例1:
先生成一批文件,为了方便,文件名都按照0、1、2...的序号来命名,每个文件内容不同(用dd+urandom生成),大小相同都是50MB的文件。
然后开始进行单线程读取10个文件的测试:

Java代码   收藏代码
  1. $ 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  
  2. single thread cost: 20312 ms  
  3. finished, total cost: 20322  


下面是测试进行过程中,系统的各项资源开销:

Java代码   收藏代码
  1. ----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--  
  2. usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw   
  3.   0   0  98   2   0   0|  17M    0 | 384B  412B|   0     0 |1139   374   
  4.   2   0  96   2   0   0|  38M    0 | 686B  522B|   0     0 |1502  1083   
  5.   4   0  96   0   0   0|  51M    0 | 448B  412B|   0     0 |1223   194   
  6.   4   0  96   0   0   0|  51M    0 | 448B  412B|   0     0 |1220   186   
  7.   4   0  95   0   0   0|  50M    0 | 812B  412B|   0     0 |1215   235   
  8.   4   0  96   0   0   0|  48M  240k| 448B  412B|   0     0 |1218   286   
  9.   4   0  96   0   0   0|  49M   24k| 512B  412B|   0     0 |1213   213   
  10.   4   0  96   0   0   0|  50M    0 | 622B  476B|   0     0 |1218   200   
  11.   4   0  96   0   0   0|  51M    0 | 384B  412B|   0     0 |1215   188   
  12.   4   0  96   0   0   0|  51M    0 | 448B  412B|   0     0 |1219   174   
  13.   4   0  94   2   0   0|  44M    0 | 448B  412B|   0     0 |1194   339   
  14.   4   0  94   2   0   0|  49M   24k| 862B  412B|   0     0 |1219   366   
  15.   4   0  96   0   0   0|  51M    0 | 384B  886B|   0     0 |1215   194   
  16.   4   0  96   0   0   0|  50M    0 | 512B 1112B|   0     0 |1218   194   
  17.   4   0  96   0   0   0|  51M    0 | 448B  412B|   0     0 |1216   184   
  18.   4   0  95   1   0   0|  49M   48k| 558B  540B|   0     0 |1216   298   
  19.   4   0  96   0   0   0|  50M   24k| 384B  412B|   0     0 |1215   272   
  20.   4   0  96   0   0   0|  50M   48k| 448B  412B|   0     0 |1220   236   
  21.   4   0  96   0   0   0|  50M  168k| 448B  412B|   0     0 |1225   187   
  22.   4   0  96   0   0   0|  51M    0 | 448B  476B|   0     0 |1217   178   
  23.   4   0  96   1   0   0|  44M    0 | 384B  412B|   0     0 |1187   180   
  24.   3   0  96   1   0   0|  38M  240k| 448B  412B|   0     0 |1208   315   


小结:程序总共耗时20s,由于是单线程读,所以cpu开销非常小,usr在4%,基本没有iowait。上下文切换开销在200~300左右。


测试用例2:
同样是这10个文件,用多线程读取(每个文件一个线程):

Java代码   收藏代码
  1. $ 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  
  2. multi thread cost: 19124 ms  
  3. finished, total cost: 19144  


下面是测试进行过程中,系统的各项资源开销:

Java代码   收藏代码
  1. ----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--  
  2. usr sys idl wai hiq siq| read  writ| recv  send|  in   out | int   csw   
  3.   1   0  95   4   0   0|  29M    0 | 512B  412B|   0     0 |1515  1402   
  4.   5   0  75  20   0   0|  54M    0 | 622B  428B|   0     0 |1253   652   
  5.   4   0  74  21   0   0|  56M    0 | 320B  318B|   0     0 |1245   601   
  6.   5   0  76  20   0   0|  56M    0 | 448B  318B|   0     0 |1242   602   
  7.   4   0  84  12   0   0|  52M    0 | 622B  476B|   0     0 |1228   600   
  8.   4   0  85  12   0   0|  45M  264k| 384B  412B|   0     0 |1201   539   
  9.   4   0  83  12   0   0|  53M    0 | 798B  886B|   0     0 |1225   588   
  10.   4   0  81  15   0   0|  54M    0 | 384B  412B|   0     0 |1234   596   
  11.   4   0  86  10   0   0|  52M   16k| 384B  412B|   0     0 |1233   597   
  12.   4   0  86   9   0   0|  53M    0 | 448B  412B|   0     0 |1237   582   
  13.   4   0  80  16   0   0|  53M    0 | 896B  412B|   0     0 |1227   593   
  14.   4   0  85  10   0   0|  52M   88k| 448B  412B|   0     0 |1238   607   
  15.   4   0  83  13   0   0|  54M    0 | 384B  412B|   0     0 |1236   607   
  16.   4   0  75  20   0   0|  55M    0 | 384B  412B|   0     0 |1238   587   
  17.   4   0  80  15   0   0|  54M    0 | 448B  412B|   0     0 |1236   588   
  18.   4   0  83  13   0   0|  46M    0 | 558B  476B|   0     0 |1200   528   
  19.   4   0  77  19   0   0|  51M   16k| 448B  412B|   0     0 |1227   605   
  20.   4   0  82  14   0   0|  52M 8192B| 448B  412B|   0     0 |1227   571   
  21.   4   0  88   7   0   0|  56M    0 | 448B  412B|   0     0 |1245   615   
  22.   4   0  94   2   0   0|  54M    0 | 384B  412B|   0     0 |1231   480   
  23.   0   0  99   1   0   0|1056k  584k| 512B  884B|   0     0 |1092   235   


由于wai比较高,所以再来看一下iostat的状况(iostat命令详解参考这里):

扫描二维码关注公众号,回复: 326610 查看本文章
Java代码   收藏代码
  1. iostat -x 1  
  2. Device:  rrqm/s  wrqm/s  r/s  w/s  rsec/s  wsec/s  avgrq-sz  avgqu-sz  await  svctm  %util  
  3. sda  103.00  2.00 228.00  0.00 54088.00     0.00   237.23     9.28   62.40   4.39 100.10  
  4. sda1  0.00  0.00  0.00  0.00     0.00     0.00     0.00     0.00    0.00   0.00   0.00  
  5. sda2  0.00  2.00  0.00  0.00     0.00     0.00     0.00     0.10    0.00   0.00   9.50  
  6. sda3  0.00  0.00  0.00  0.00     0.00     0.00     0.00     0.00    0.00   0.00   0.00  
  7. sda4  0.00  0.00  0.00  0.00     0.00     0.00     0.00     0.00    0.00   0.00   0.00  
  8. sda5  103.00  0.00 228.00  0.00 54088.00     0.00   237.23     9.18   62.40   4.39 100.10  
  9.   
  10. avg-cpu:  %user   %nice %system %iowait  %steal   %idle  
  11.            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

猜你喜欢

转载自m635674608.iteye.com/blog/2389173