多线程实例(二)——遍历文件夹分割文件识别文件内容
上篇写完,感觉作为一个程序员,没有撸到底好像有点不过瘾对不对?大家都知道,C#早已进阶到8.0时代了,还用原始的Thread来写感觉有点low呀,而且通篇到最后居然还有线程最大值限制,技术控不能忍!!!
那么本篇就干脆继续优化,理想状态是8秒,我就必须将整个过程压缩到8秒这个量级!而且尽量使用新技术。
1.引入线程池ThreadPool,来控制线程数,提高效率。
2.引入CountdownEvent同步基元,通过它的信号计数来确定多线程是否成功完成。
如何获取线程池能够使用的最大线程数和最小线程数呢?
1 static void Main(string[] args) 2 { 3 ThreadPool.GetMaxThreads(out nMaxThread, out nMaxThread_IO); 4 strInfo = $"nMaxThread : {nMaxThread}, nMaxThread_async : {nMaxThread_IO}."; 5 Console.WriteLine(strInfo); 6 ThreadPool.GetMinThreads(out nMinThread, out nMinThread_IO); 7 strInfo = $"nMinThread : {nMinThread}, nMinThread_async : {nMinThread_IO}."; 8 Console.WriteLine(strInfo); 9 Console.ReadKey(); 10 }
根据操作系统和CPU硬件不同,得到的值也有所不同,我们这里需要先记录下来这几个阈值,后面优化时需要用到。
接下来,我们可以对比一下使用线程池和使用线程的性能。
1 class Program 2 { 3 private static readonly Stopwatch sw = new Stopwatch(); 4 private static string strInfo; 5 6 static void Main(string[] args) 7 { 8 sw.Start(); 9 strInfo = $"Enter Main : {sw.ElapsedMilliseconds} ms"; 10 Console.WriteLine(strInfo); 11 const int numberOfThreads = 300; 12 sw.Reset(); 13 sw.Start(); 14 UseThreadPool(numberOfThreads); 15 sw.Stop(); 16 Console.WriteLine("Execution time using threadpool: {0}", sw.ElapsedMilliseconds); 17 18 sw.Reset(); 19 sw.Start(); 20 UseThreads(numberOfThreads); 21 sw.Stop(); 22 Console.WriteLine("Execution time using threads: {0}", sw.ElapsedMilliseconds); 23 Console.ReadKey(); 24 } 25 26 static void UseThreads(int numberOfThreads) 27 { 28 using (var countdown = new CountdownEvent(numberOfThreads)) 29 { 30 Console.WriteLine("Scheduling work by creating threads"); 31 for (int i = 0; i < numberOfThreads; i++) 32 { 33 var thread = new Thread(() => { 34 Thread.Sleep(TimeSpan.FromSeconds(0.1)); 35 Console.Write("{0} ", Thread.CurrentThread.ManagedThreadId); 36 countdown.Signal(); 37 }); 38 thread.Start(); 39 } 40 countdown.Wait(); 41 Console.WriteLine(); 42 } 43 } 44 45 static void UseThreadPool(int numberOfThreads) 46 { 47 using (var countdown = new CountdownEvent(numberOfThreads)) 48 { 49 Console.WriteLine("Starting work on a threadpool"); 50 for (int i = 0; i < numberOfThreads; i++) 51 { 52 ThreadPool.QueueUserWorkItem(_ => { 53 Thread.Sleep(TimeSpan.FromSeconds(0.1)); 54 Console.Write("{0} ", Thread.CurrentThread.ManagedThreadId); 55 countdown.Signal(); 56 }); 57 } 58 countdown.Wait(); 59 Console.WriteLine(); 60 } 61 } 62 }
此例可以看出线程池反复利用10~15这几个线程,大量节约了创建线程,分配资源等的时间消耗。
OK,既然验证有效,我们不妨开始按照预先的构思,开始编码吧!
1 class Program 2 { 3 private static readonly Stopwatch sw = new Stopwatch(); 4 private static string strInfo; 5 6 static void Main(string[] args) 7 { 8 sw.Start(); 9 strInfo = $"Enter Main : {sw.ElapsedMilliseconds} ms"; 10 Console.WriteLine(strInfo); 11 12 string strFilefolder = ""; 13 OcrProcess(strFilefolder); 14 strInfo = $"Main Completed : {sw.ElapsedMilliseconds} ms"; 15 Console.WriteLine(strInfo); 16 sw.Stop(); 17 Console.ReadKey(); 18 } 19 20 static void OcrProcess(string strFilefolder) 21 { 22 List<string> list_sourcefile = GetFileList(strFilefolder); 23 using (var countdown = new CountdownEvent(list_sourcefile.Count)) 24 { 25 list_sourcefile.ForEach((sourcefile) => 26 { 27 ThreadPool.QueueUserWorkItem(_ => 28 { 29 strInfo = $"{sourcefile} : {sw.ElapsedMilliseconds} ms"; 30 Console.WriteLine(strInfo); 31 //这里对文件进行分割 32 SplitProcess(sourcefile); 33 countdown.Signal(); 34 }); 35 }); 36 countdown.Wait(); 37 } 38 } 39 40 static void SplitProcess(string sourcefile) 41 { 42 strInfo = $"{sourcefile} Split Start : {sw.ElapsedMilliseconds} ms"; 43 Console.WriteLine(strInfo); 44 int nSplitNum = 6; 45 using (var countdown = new CountdownEvent(nSplitNum)) 46 { 47 for (int i = 0; i < nSplitNum; i++) 48 { 49 //模拟分割单个文件的过程,花费500ms 50 Thread.Sleep(500); 51 string split_file = sourcefile + i; 52 strInfo = $"{split_file} Ready : {sw.ElapsedMilliseconds} ms"; 53 Console.WriteLine(strInfo); 54 ThreadPool.QueueUserWorkItem(_ => 55 { 56 RecognizeProcess(split_file); 57 countdown.Signal(); 58 }); 59 } 60 countdown.Wait(); 61 } 62 strInfo = $"{sourcefile} Split Completed : {sw.ElapsedMilliseconds} ms"; 63 Console.WriteLine(strInfo); 64 } 65 66 static void RecognizeProcess(string split_file) 67 { 68 //模拟识别的过程,花费5000ms 69 Thread.Sleep(5000); 70 strInfo = $"{split_file} OCR completed : {sw.ElapsedMilliseconds} ms"; 71 Console.WriteLine(strInfo); 72 } 73 74 static List<string> GetFileList(string strFilefolder) 75 { 76 List<string> list_file = new List<string>(); 77 for (int i = 0; i <= 2; i++) 78 { 79 for (int j = 0; j <= 2; j++) 80 list_file.Add("File" + i + j); 81 } 82 return list_file; 83 } 84 85 }
可惜,执行结果居然耗时32秒多。
分析:我们的代码有两个地方都用到了线程池,第一个地方是想要同步切割原始文件时,第二个地方是每次识别切割文件时,就需要从线程池分配一个线程去识别,因为识别耗时较长。
从这里的输出来看,①231ms和232ms,②239ms和247ms,③759ms和780ms,④1199ms和1199ms,似乎同时只有两个线程(可以理解为工人)在并发执行,推断应该是线程池的最小线程数(nMinThread=2)在起作用,也就是说,当任务数量大于工人数量(最小线程数)时,线程池每次最多派出(nMinThread)2个工人,各自领取一个任务去做,其余的任务则继续在线程池里等待(当这两个工人任务完成恢复空闲时,才会被线程池指派去做后面的任务),导致了耗时。
结论:通过设置线程池的最小线程数(nMinThread=2),可以提高并发执行数,提高效率。
要达到最快的话,9个文件的分割需要9个线程,每个原始文件分割为6个子文件,所以需要识别的文件为6*9=54个文件,每次识别需要1个线程,总共则需要63个线程,这明显也小于系统1000的阈值限制,可以放心设置,使任务不再处于等待状态。
1 ThreadPool.SetMinThreads(63, 63);
最终结果:
执行结果耗时8秒多,达到了预期,感兴趣的话可以改变最小线程数(nMinThread)的值,观察耗时的改变,加深对多线程的理解。