1. 程式人生 > 其它 >C#.net多執行緒並行處理例項:處理資料夾及巢狀資料夾下所有檔案內容字串

C#.net多執行緒並行處理例項:處理資料夾及巢狀資料夾下所有檔案內容字串

需求

某系統的邏輯資料庫表結構以檔案形式儲存,為了分析該資料庫及表結構,需對各表文件進行處理,儲存到指定表中以供分析。

檔案結構下圖所示。包含有1k個資料夾,末級檔案單位大概4w多個。需要讀取每個檔案內容,將欄位和表名儲存到oracle庫表。並且監視各資料夾,若有更新,則及時將變更同步到表中。

分析

c#的檔案處理比較容易實現,選擇c#語言做這個工具。思路是:選擇檔案目錄,然後根據使用者指定的掃描頻率進行檔案更新操作監控。如有新檔案,則執行讀取、分析、寫表操作。

遇到的問題

1、檔案數量越來越多,第一次將4w多檔案執行讀取時,就用的foreach檔案迴圈,執行了1個多小時,也沒有執行完,在同事提醒下,使用多執行緒。

2、在查了資料後,選擇使用“AsParallel”功能來實現並行,並設定WithDegreeOfParallelism程序數來調整並行程序數量(並不是越多越好,要根據執行機器cpu情況)。

3、由於資料夾數量眾多,各資料夾下檔案也多少不一,選擇在資料夾掃描 和 檔案處理 兩個操作上都使用並行處理。

4、單個檔案讀取時,最初使用按行讀取,比較耗時,然後想到一個檔案所有行讀取到一個數組,陣列元素依然可以並行處理。

5、起初按照一個檔案行做處理,將表面+欄位名抽取,執行一個sql語句寫表。由於連線資料庫數量過多,效率很低,改為每個檔案各行sql語句連線起來,執行一次資料庫寫入操作。

程式碼實現

        窗體初始化時,加入程式碼:

Control.CheckForIllegalCrossThreadCalls 
= false;// 允許跨程序訪問控制元件 第一步:選擇資料夾: FolderBrowserDialog dilog = new FolderBrowserDialog(); dilog.Description = "請選擇資料夾"; if (dilog.ShowDialog() == DialogResult.OK || dilog.ShowDialog() == DialogResult.Yes) { textBox1.Text = dilog.SelectedPath; // 記錄操作資料夾路徑 } 第二步:點選監控按鈕,選擇有最新修改的檔案進行處理: if (pTimer != null
) {// 多次點選監控時,先停止之前開啟的監聽操作; pTimer.Stop(); pTimer = null; } path = textBox1.Text.Trim(); if (string.IsNullOrEmpty(path)) { label2.Text = textBox2.Text = "路徑非法!"; return; } // 啟動時,獲取上次時間 try { string filepath = path + "//last.txt"; //要上傳的資料夾的路徑 if (!File.Exists(filepath)) //不存在檔案,建立 { FileStream fs = File.Create(filepath); //建立 fs.Close(); //記錄最終時間 lastRead = DateTime.Parse("2021-01-01 11:03:24"); //更新最後操作時間 writeFile(); } StreamReader sr = new StreamReader(path + "//last.txt", Encoding.GetEncoding("GB2312")); strLine = sr.ReadLine(); if (strLine != null) { Console.WriteLine(strLine); lastRead = DateTime.Parse(strLine); label2.Text = textBox2.Text = "上次獲取時間節點為:" + lastRead.ToString("yyyy-MM-dd HH:mm:ss"); } sr.Dispose(); sr.Close(); } catch (Exception ex) { label2.Text = "獲取最後修改日期 處理錯誤" + ex.Message; lastRead = DateTime.Now; } checkFiles(null, null); //定時執行 pTimer = new System.Timers.Timer(20000);//每隔20秒執行一次,沒用winfrom自帶的 pTimer.Elapsed += checkFiles;//委託,要執行的方法 pTimer.AutoReset = true;//獲取該定時器自動執行 pTimer.Enabled = true;//這個一定要寫,要不然定時器不會執行的 第三步:層級訪問資料夾函式: private void checkFiles(object sender, System.Timers.ElapsedEventArgs e) { // 引入執行標識,如果在執行狀態下,則退出 if (bz) return; // 設定標識狀態和檔案數量為初始值; bz = true; filesCount = 0; // 記錄執行時間,用於對比是否提高執行效率 Stopwatch watch = new Stopwatch(); watch.Start(); // 獲取資料夾及子資料夾 的檔案,識別表名、欄位名 getFolders(path); // 停止記錄執行時間 watch.Stop(); Console.WriteLine("總共開銷{0}分鐘。", watch.ElapsedMilliseconds / (1000 * 60)); //記錄最終時間 lastRead = DateTime.Now; label2.Text = "該時間段共有 " + filesCount.ToString() + " 個檔案:" + lastRead.ToString("yyyy-MM-dd HH:mm:ss") + ",耗時總共 " + (watch.ElapsedMilliseconds / (1000 * 60)).ToString() + " 分鐘。"; //更新最後操作時間 writeFile(); //恢復標識,讓輪詢任務繼續執行 bz = false; } // 展開資料夾及子目錄資料夾 private void getFolders(string rootPath) { if (textBox2.Lines.Length > 2000) textBox2.Text = "";// 判斷日誌長度 getNewFiles(rootPath); // 處理該資料夾下檔案並 記錄檔案數量; DirectoryInfo root = new DirectoryInfo(rootPath); // 資料夾並行處理 root.GetDirectories().AsParallel().WithDegreeOfParallelism(2).ForAll(d => getFolders(d.FullName)); // 序列處理 //foreach (DirectoryInfo d in root.GetDirectories()) //{ // getFolders(d.FullName); //} } // 讀取檔案 private void getNewFiles(string folderPath) { DirectoryInfo root = new DirectoryInfo(folderPath); // 檔案並行處理 root.GetFiles().AsParallel().WithDegreeOfParallelism(4).ForAll(f => readFile(f, root.Name)); } // 單個檔案處理函式 private void readFile(FileInfo file, string folderName) { // 判斷規則 if (file.Name.Contains(".") || file.LastWriteTime < lastRead) return; string strLine = "", tname = "", sname = ""; filesCount++; textBox2.Text += folderName + " " + file.FullName + "\r\n"; //讀取該檔案所有行到陣列; string[] flines = System.IO.File.ReadAllLines(file.FullName); // var matchLines = (from line in flines.AsParallel() where (line.IndexOf(":NAME:") >= 0 && line.IndexOf('.') > 0) select line); StringBuilder sb = new StringBuilder(); if (matchLines.Count<string>() > 0) { sb.AppendLine("BEGIN "); foreach (string line in matchLines) { strLine = line.Replace(":NAME:", ""); tname = strLine.Substring(0, strLine.IndexOf('.')); sname = strLine.Replace(tname + ".", ""); sb.AppendLine( "EXECUTE IMMEDIATE 'INSERT INTO tables select ''" + tname + "'', ''" + sname + "'', ''" + folderName + "'' from dual " + "where not exists(select 1 from tables where tablename=''" + tname + "'' and colname=''" + sname + "'')';"); } sb.AppendLine("END; "); } if (sb.Length > 0) { try { string _connStr1 = ConfigurationManager.ConnectionStrings["orcl"].ToString(); OracleHelper.ExecuteNonQuery(_connStr1, CommandType.Text, sb.ToString()); } catch (Exception ex) { label2.Text = file.FullName + " 處理錯誤" + ex.Message; } } } // 寫檔案操作 private void writeFile() { //建立檔案流 FileStream myfs = new FileStream(path + "//last.txt", FileMode.Open); //開啟方式 //1:Create 用指定的名稱建立一個新檔案,如果檔案已經存在則改寫舊檔案 //2:CreateNew 建立一個檔案,如果檔案存在會發生異常,提示檔案已經存在 //3:Open 開啟一個檔案 指定的檔案必須存在,否則會發生異常 //4:OpenOrCreate 開啟一個檔案,如果檔案不存在則用指定的名稱新建一個檔案並開啟它. //5:Append 開啟現有檔案,並在檔案尾部追加內容. //建立寫入器 StreamWriter mySw = new StreamWriter(myfs);//將檔案流給寫入器 //將錄入的內容寫入檔案 mySw.Write(lastRead); //關閉寫入器 mySw.Close(); //關閉檔案流 myfs.Close(); }

優化意見

從最初執行40多分鐘(沒有執行完便關閉了),到執行20多分鐘(沒有執行完關掉了),到最後3分鐘。並行處理起了很大的作用。還有優化的空間不吝賜教。

參考的檔案

1、使用.NET進行多執行緒檔案處理 http://www.voidcn.com/article/p-wntpjeyo-bte.html

2、C#讀取檔案所有行到陣列的方法https://www.jb51.net/article/68885.htm

3、C# 並行程式設計 之 PLINQ並行度的指定 和 ForAll的使用 https://blog.csdn.net/wangzhiyu1980/article/details/46355633

4、執行緒間操作無效:從不是建立控制元件“textBox1”的執行緒訪問它https://www.cnblogs.com/xunzhiyou/p/4931506.html

5、C# Oracle同時執行多條sql語句http://www.voidcn.com/article/p-kwkokmkd-so.html

by Vincent