多线程实例(二)——遍历文件夹分割文件识别文件内容

上篇写完,感觉作为一个程序员,没有撸到底好像有点不过瘾对不对?大家都知道,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         }
View Code

 根据操作系统和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     }
View Code

 此例可以看出线程池反复利用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     }
View Code

 可惜,执行结果居然耗时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)的值,观察耗时的改变,加深对多线程的理解。

 

posted @ 2020-06-29 16:33  潮崖客  阅读(456)  评论(0编辑  收藏  举报