上篇写完,感觉作为一个程序员,没有撸到底好像有点不过瘾对不对?大家都知道,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)的值,观察耗时的改变,加深对多线程的理解。