需求:遍历文件夹下的所有pdf文件,对每个pdf文件根据二维码进行分割,再对分割后的文件的内容进行识别。
可以拆分为以下几个关键方法:
1.GetFileList方法:遍历文件,获取源文件动态数组(这里假设3个文件夹,每个文件夹下有3个文件,则源文件个数为9),耗时忽略不计


1 static List<string> GetFileList(string strFilefolder)
2 {
3 List<string> list_file = new List<string>();
4
5 for (int i = 0; i <= 2; i++)
6 {
7 for (int j = 0; j <= 2; j++)
8 list_file.Add("File" + i + j);
9 }
10
11 return list_file;
12 }
2.SplitProcess方法:分割原始pdf文件,识别二维码(假设耗时500ms),将一个pdf文件分割为N(这里假设个数为6)个子文件


1 static void SplitProcess(string sourcefile)
2 {
3 Console.WriteLine("SplitFile Start:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
4 for (int i = 0; i <= 5; i++)
5 {
6 //模拟分割单个文件的过程,花费500ms
7 Thread.Sleep(500);
8 string split_file = sourcefile + i;
9 Console.WriteLine("file ready:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
10 RecognizeProcess(split_file);
11 }
12 Console.WriteLine("SplitFile Completed:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
13 }
3.RecognizeProcess方法:识别子文件的内容:加载识别库,设置识别参数,截取识别区域图像,图像处理(如缩放,降噪,灰度转换等),识别(假设耗时5000ms)


1 static void RecognizeProcess(string split_file)
2 {
3 //模拟识别的过程,花费5000ms
4 Thread.Sleep(5000);
5 Console.WriteLine("ocrFile Completed:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
6 }
单线程处理:


1 static void Main(string[] args)
2 {
3 Console.WriteLine("Enter Main" + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
4 string strFilefolder = "";
5 OcrProcess(strFilefolder);
6 Console.WriteLine("Main Completed" + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
7 Console.ReadKey();
8 }
9
10 static void OcrProcess(string strFilefolder)
11 {
12 List<string> list_sourcefile = GetFileList(strFilefolder);
13 list_sourcefile.ForEach((sourcefile) =>
14 {
15 Console.WriteLine(sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
16 //这里对文件进行分割
17 SplitProcess(sourcefile);
18 });
19 }
这个单线程处理的执行结果我们可以预估以下,应该大于 9 * 6 * (0.5 + 5) = 297 秒。
实际结果:
……
开始时间 2020-06-17 15:22:28 6104 结束时间 2020-06-17 15:27:26 1541
由于是线性处理,整个过程耗费的时间约5分钟,所以必须要进行优化,所以考虑用多线程来提高效率。
优化方向:
1.多线程,使用Task并行对源文件进行分割


1 static void OcrProcess(string strFilefolder)
2 {
3 List<Task> tasks = new List<Task>();
4 List<string> list_sourcefile = GetFileList(strFilefolder);
5 list_sourcefile.ForEach((sourcefile) =>
6 {
7 Task task = Task.Factory.StartNew( () =>
8 {
9 Console.WriteLine(sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
10 //这里对文件进行分割
11 SplitProcess(sourcefile);
12 });
13 tasks.Add(task);
14 });
15 Task.WaitAll(tasks.ToArray());
16 }
……
开始时间 2020-06-17 15:51:54 5458 结束时间 2020-06-17 15:52:35 3144
整个过程耗费的时间约41秒,优化效果明显。
2.每分割出来一个文件,开启子线程,进行识别


1 static void SplitProcess(string sourcefile)
2 {
3 List<Task> tasks = new List<Task>();
4 Console.WriteLine("SplitFile Start:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
5 for (int i = 0; i <= 5; i++)
6 {
7 //模拟分割单个文件的过程,花费500ms
8 Thread.Sleep(500);
9 string split_file = sourcefile + i;
10 Console.WriteLine("file ready:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
11 Task task = Task.Factory.StartNew(() =>
12 {
13 RecognizeProcess(split_file);
14 });
15 tasks.Add(task);
16 }
17 Task.WaitAll(tasks.ToArray());
18 Console.WriteLine("SplitFile Completed:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
19 }
……
开始时间 2020-06-17 15:58:59 2591 结束时间 2020-06-17 15:59:28 9051
整个过程耗费的时间约29秒,运行时间进一步缩短。
然而,最后再思考一下,如果把多线程发挥到极致,理想状态应该是多少秒执行完毕?
500ms:第一个文件被分割出来,在这个时间里,线程同步的话,那么后续文件也一并被分割出来了。
5500ms:第一个分割文件已识别,在这个时间里,线程同步的话,后续文件应该也一并都被识别出来了。
所以,理想情况下,应该是5.5秒,而与29秒差距太大了,应该还有优化空间!
3.怎么优化?向什么方向优化?我们不妨不用Task,回归到Thread本身来试试。
可是Thread运行时没有Task.WaitAll()这样的控制方法,因此,我们还要引入WaitHandle和ManualResetEvent来进行多线程管理。


1 class Program
2 {
3 static void Main(string[] args)
4 {
5 Console.WriteLine("Enter Main" + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
6 string strFilefolder = "";
7 OcrProcess(strFilefolder);
8 Console.WriteLine("Main Completed" + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
9 Console.ReadKey();
10 }
11
12 static void OcrProcess(string strFilefolder)
13 {
14 List<ManualResetEvent> split_waits = new List<ManualResetEvent>();
15 List<string> list_sourcefile = GetFileList(strFilefolder);
16 list_sourcefile.ForEach((sourcefile) =>
17 {
18 Thread m_thread = new Thread(() =>
19 {
20 ManualResetEvent mre = new ManualResetEvent(false);
21 split_waits.Add(mre);
22 Console.WriteLine(sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
23 //这里对文件进行分割
24 SplitProcess(sourcefile);
25 mre.Set();
26 });
27 m_thread.Start();
28 });
29 WaitHandle.WaitAll(split_waits.ToArray());
30 }
31
32 static void SplitProcess(string sourcefile)
33 {
34 Console.WriteLine("SplitFile Start:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
35 var ocr_waits = new List<EventWaitHandle>();
36 for (int i = 0; i <= 5; i++)
37 {
38 //模拟分割单个文件的过程,花费500ms
39 Thread.Sleep(500);
40 string split_file = sourcefile + i;
41 Console.WriteLine("file ready:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
42 ManualResetEvent mre_child = new ManualResetEvent(false);
43 ocr_waits.Add(mre_child);
44 Thread m_child_thread = new Thread(() =>
45 {
46 Console.WriteLine("m_child_thread enter:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
47 RecognizeProcess(split_file);
48 mre_child.Set();
49 Console.WriteLine("m_child_thread after set:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
50 });
51 m_child_thread.Start();
52 }
53 WaitHandle.WaitAll(ocr_waits.ToArray());
54 Console.WriteLine("SplitFile Completed:" + sourcefile + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
55 }
56
57 static void RecognizeProcess(string split_file)
58 {
59 //模拟识别的过程,花费5000ms
60 Thread.Sleep(5000);
61 Console.WriteLine("ocrFile Completed:" + split_file + DateTime.Now.ToString(" yyyy-MM-dd HH:mm:ss ffff"));
62 }
63
64 static List<string> GetFileList(string strFilefolder)
65 {
66 List<string> list_file = new List<string>();
67 for (int i = 0; i <= 2; i++)
68 {
69 for (int j = 0; j <= 2; j++)
70 list_file.Add("File" + i + j);
71 }
72 return list_file;
73 }
74 }
……
开始时间 2020-06-17 15:28:17 2397 结束时间 2020-06-17 16:28:27 9151
整个过程耗费的时间约10秒,运行时间与理论的5.5秒已经十分接近(因为Thread切换上下文,Console.WriteLine都有一定的耗时),可以说目标已经达成。
Tips:
ManualResetEvent初始状态为false表示不将线程信号量初始值置为signal,线程会自动往下执行,执行Set()方法时,将线程信号量置为signal。
WaitHandle.WaitAll(split_waithandle1,split_waithandle2); //一直等待,直到split_waithandle1,split_waithandle2信号量均被置为signal才会往下执行。
不足之处:
开启Thread要受到系统的限制,所以本例线程数必须考虑操作系统线程最大值限制。
来源:oschina
链接:https://my.oschina.net/u/4284321/blog/4315425