使用Ring Buffer構建高性能的文件寫入程式

来源:http://www.cnblogs.com/bluedoctor/archive/2016/09/21/5892726.html
-Advertisement-
Play Games

什麼是Ring Buffer?顧名思義,就是一個記憶體環,每一次讀寫操作都迴圈利用這個記憶體環,從而避免頻繁分配和回收記憶體,減輕GC壓力,同時由於Ring Buffer可以實現為無鎖的隊列,從而整體上大幅提高系統性能。 ...



最近常收到SOD框架的朋友報告的SOD的SQL日誌功能報錯:文件句柄丟失。經過分析得知,這些朋友使用SOD框架開發了訪問量比較大的系統,由於忘記關閉SQL日誌功能所以出現了很高頻率的日誌寫入操作,從而偶然引起錯誤。後來我建議只記錄出錯的或者執行時間較長的SQL信息,暫時解決了此問題。但是作為一個熱心造輪子的人,一定要看看能不能造一個更好的輪子出來。

前面說的錯誤原因已經很直白了,就是頻繁的日誌寫入導致的,那麼解決方案就是將多次寫入操作合併成一次寫入操作,並且採用非同步寫入方式。要保存多次操作的內容就要有一個類似“隊列”的東西來保存,而一般的線程安全的隊列,都是“有鎖隊列”,在性能要求很高的系統中,不希望在日誌記錄這個地方耗費多一點計算資源,所以最好有一個“無鎖隊列”,因此最佳方案就是Ring Buffer(環形緩衝區)了。

 什麼是Ring Buffer?顧名思義,就是一個記憶體環,每一次讀寫操作都迴圈利用這個記憶體環,從而避免頻繁分配和回收記憶體,減輕GC壓力,同時由於Ring Buffer可以實現為無鎖的隊列,從而整體上大幅提高系統性能。Ring Buffer的示意圖如下,有關具體原理,請參考此文《Ring Buffer 有什麼特別? 》。

 Ring buffer

上文並沒有詳細說明如何具體讀寫Ring Buffer,但是原理介紹已經足夠我們怎麼寫一個Ring Buffer程式了,接下來看看我在 .NET上的實現。

首先,定一個存放數據的數組,記住一定要用數組,它是實現Ring Buffer的關鍵並且CPU友好。

const int C_BUFFER_SIZE = 10;//寫入次數緩衝區大小,每次的實際內容大小不固定
string[] RingBuffer = new string[C_BUFFER_SIZE];
int writedTimes = 0;


變數writedTimes 記錄寫入次數,它會一直遞增,不過為了線程安全的遞增且不使用托管鎖,需要使用原子鎖Interlocked。之後,根據每次 writedTimes 跟環形緩衝區的大小求餘數,得到當前要寫入的數組位置:

 void SaveFile(string fileName, string text)
 {
            int currP= Interlocked.Increment(ref writedTimes);
            int writeP= currP % C_BUFFER_SIZE ;
            int index = writeP == 0 ? C_BUFFER_SIZE - 1 : writeP - 1;
            RingBuffer[index] = " Arr[" + index + "]:" + text;
  }


Ring Buffer的核心代碼就這麼點,調用此方法,會一直往緩衝區寫入數據而不會“溢出”,所以寫入Ring Buffer效率很高。


一個隊列如果只生產不消費肯定不行的,那麼如何及時消費Ring Buffer的數據呢?簡單的方案就是當Ring Buffer“寫滿”的時候一次性將數據“消費”掉。註意這裡的“寫滿”僅僅是指寫入位置 index達到了數組最大索引位置,而“消費”也不同於常見的堆棧,隊列等數據結構,只是讀取緩衝區的數據而不會移除它。

所以前面的代碼只需要稍加改造:

 void SaveFile(string fileName, string text)
 {
            int currP= Interlocked.Increment(ref writedTimes);
            int writeP= currP % C_BUFFER_SIZE ;
            int index = writeP == 0 ? C_BUFFER_SIZE - 1 : writeP - 1;
            RingBuffer[index] = " Arr[" + index + "]:" + text;
            if (writeP == 0 )
            {
                 string result = string.Concat( RingBuffer);
                 FlushFile(fileName, result);
            }
  }


writeP == 0 表示當前一輪的緩衝區已經寫滿,然後調用函數 FlushFile 將Ring Buffer的數據連接起來,整體寫入文件。

        void FlushFile(string fileName, string text)
        {
            using (FileStream fs = new FileStream(fileName, FileMode.Append, FileAccess.Write, FileShare.Write, 2048, FileOptions.Asynchronous))
            {
                byte[] buffer = System.Text.Encoding.UTF8.GetBytes(text);
                IAsyncResult writeResult = fs.BeginWrite(buffer, 0, buffer.Length,
                    (asyncResult) =>
                    {
                        fs.EndWrite(asyncResult);
                       
                    },
                    fs);
                //fs.EndWrite(writeResult);//這種方法非同步起不到效果
                fs.Flush();
                
            }
        }

在函數 FlushFile 中我們使用了非同步寫入文件的技術,註意 FileOptions.Asynchronous ,使用它才可以真正利用Windows的完成埠IOCP,將文件非同步寫入。

當然這段代碼也可以使用.NET最新版本支持的 async/await ,不過我要讓SOD框架繼續支持.NET 2.0,所以只好這樣寫了。

現在,我們可以開多線程來測試這個迴圈隊列效果怎麼樣:

            Task[] arrTask = new Task[20];
            for (int i = 0; i < arrTask.Length; i++)
            {
                arrTask[i] = new Task(obj => SaveFile( (int)obj) ,i);
            }
            for (int i = 0; i < arrTask.Length; i++)
            {
                arrTask[i].Start();
            }
            
            Task.WaitAll(arrTask);
            MessageBox.Show(arrTask.Length +" Task All OK.");

這裡開啟20個Task任務線程來寫入文件,運行此程式,發現20個線程才寫入了10條數據,分析很久才發現,文件非同步IO太快的話,會有緩衝區丟失,第一次寫入的10條數據無法寫入文件,多運行幾次就沒有問題了。所以還是得想法解決此問題。

通常情況下我們都是使用托管鎖來解決這種併發問題,但本文的目的就是要實現一個“無鎖環形緩衝區”,不能在此“功虧一簣”,所以此時“信號量”上場了。

同步可以分為鎖定和信號同步,信號同步機制中涉及的類型都繼承自抽象類WaitHandle,這些類型有EventWaitHandle(類型化為AutoResetEvent、ManualResetEvent)、Semaphore以及Mutex。見下圖:

首先聲明一個 ManualResetEvent對象:

ManualResetEvent ChangeEvent = new ManualResetEvent(true);

這裡我們將 ManualResetEvent 對象設置成 “終止狀態”,意味著程式一開始是允許所有線程不等待的,當我們需要消費Ring Buffer的時候再將  ManualResetEvent 設置成“非終止狀態”,阻塞其它線程。簡單說就是當要寫文件的時候將環形緩衝區阻塞,直到文件寫完才允許繼續寫入環形緩衝區。

對應的新的代碼調整如下:

 void SaveFile(string fileName, string text)
 {
            ChangeEvent.WaitOne();
            int currP= Interlocked.Increment(ref writedTimes);
            int writeP= currP % C_BUFFER_SIZE ;
            int index = writeP == 0 ? C_BUFFER_SIZE - 1 : writeP - 1;
            RingBuffer[index] = " Arr[" + index + "]:" + text;
            if (writeP == 0 )
            {
                 ChangeEvent.Reset();
                 string result = string.Concat( RingBuffer);
                 FlushFile(fileName, result);
            }
  }

然後,再FlushFile 方法的 回掉方法中,加入設置終止狀態的代碼,部分代碼如下:

(asyncResult) =>
                    {
                        fs.EndWrite(asyncResult);
                        ChangeEvent.Set();
                     }

OK,現在我們的程式具備高性能的安全的寫入日誌文件的功能了,我們來看看演示程式測試的日誌結果實例:

 Arr[0]:Thread index:0--FFFFFFF
 Arr[1]:Thread index:1--FFFFFFF
 Arr[2]:Thread index:8--FFFFFFF
 Arr[3]:Thread index:9--FFFFFFF
 Arr[4]:Thread index:3--FFFFFFF
 Arr[5]:Thread index:2--FFFFFFF
 Arr[6]:Thread index:4--FFFFFFF
 Arr[7]:Thread index:10--FFFFFFF
 Arr[8]:Thread index:5--FFFFFFF
 Arr[9]:Thread index:6--FFFFFFF
 Arr[0]:Thread index:7--FFFFFFF
 Arr[1]:Thread index:11--FFFFFFF
 Arr[2]:Thread index:12--FFFFFFF
 Arr[3]:Thread index:13--FFFFFFF
 Arr[4]:Thread index:14--FFFFFFF
 Arr[5]:Thread index:15--FFFFFFF
 Arr[6]:Thread index:16--FFFFFFF
 Arr[7]:Thread index:17--FFFFFFF
 Arr[8]:Thread index:18--FFFFFFF
 Arr[9]:Thread index:19--FFFFFFF

測試結果符合預期!
到此,我們今天的主題就全部介紹完成了,不過要讓本文的代碼能夠符合實際的運行,還要解決每次只寫入少量數據並且將它定期寫入日誌文件的問題,這裡貼出真正的局部代碼:

 

PS:有朋友說採用信號量並不能完全保證程式安全,查閱了MSDN也說如果信號量狀態改變還沒有來得及應用,那麼是起不到作用的,所以還需要檢查業務狀態標記,也就是在設置非終止狀態後,馬上設置一個操作標記,在其它線程中,需要檢查此標記,以避免“漏網之魚”引起不期望的結果。

再具體實現上,我們可以實現一個“自旋鎖”,迴圈檢查此狀態標記,為了防止發生死鎖,還需要有鎖超時機制,代碼如下:

 void SaveFile(string fileName, string text)
        {
            ChangeEvent.WaitOne(10000);
            int currP= Interlocked.Increment(ref WritedTimes);
            int writeP= currP % C_BUFFER_SIZE ;
            int index = writeP == 0 ? C_BUFFER_SIZE - 1 : writeP - 1;
           
            if (writeP == 0 )
            {
                ChangeEvent.Reset();
                IsReading = true;
                RingBuffer[index] = " Arr[" + index + "]:" + text;

                LastWriteTime = DateTime.Now;
                WritingIndex = 0;
                SaveFile(fileName,RingBuffer);
            }
            else if (DateTime.Now.Subtract(LastWriteTime).TotalSeconds > C_WRITE_TIMESPAN)
            {
                ChangeEvent.Reset();
                IsReading = true;
                RingBuffer[index] = " Arr[" + index + "]:" + text;

                int length = index - WritingIndex + 1;
                if (length <= 0)
                    length = 1;
                string[] newArr = new string[length];
                Array.Copy(RingBuffer, WritingIndex, newArr, 0, length);

                LastWriteTime = DateTime.Now;
                WritingIndex = index + 1;
                SaveFile(fileName, newArr);
            }
            else
            {
                //防止漏網之魚的線程在信號量產生作用之前修改數據
                //採用“自旋鎖”等待
                int count = 0;
                while (IsReading)
                {
                    if (count++ > 10000000)
                    {
                        Thread.Sleep(50);
                        break;
                    }
                }
                RingBuffer[index] = " Arr[" + index + "]:" + text;
            }
        }

 


完整的Ring Buffer代碼會在最新版本的SOD框架源碼中,有關本篇文章測試程式的完整源碼,請加QQ群討論獲取,

群號碼:SOD框架高級群 18215717 ,加群請註明 PDF.NET技術交流 ,否則可能被拒絕。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 版權聲明 版權聲明:原創文章 禁止轉載 請通過右側公告中的“聯繫郵箱([email protected])”聯繫我 勿用於學術性引用。 勿用於商業出版、商業印刷、商業引用以及其他商業用途。 本文不定期修正完善。 本文鏈接:http://www.cnblogs.com/wlsandwho/p/ ...
  • 1、實現功能 向關註了微信公眾號的微信用戶群發消息。(可以是所有的用戶,也可以是提供了微信openid的微信用戶集合) 2、基本步驟 前提: 已經有認證的公眾號或者測試公眾賬號 發送消息步驟: 相關微信介面的信息可以查看:http://www.cnblogs.com/0201zcr/p/586629 ...
  • Java中的Bigdecimal類型運算 雙精度浮點型變數double可以處理16位有效數。在實際應用中,需要對更大或者更小的數進行運算和處理。Java在java.math包中提 供的API類BigDecimal,用來對超過16位有效位的數進行精確的運算。表5.7中列出了BigDecimal類的主要 ...
  • C - NP-Hard Problem Crawling in process... Crawling failed Time Limit:2000MS Memory Limit:262144KB 64bit IO Format:%I64d & %I64u C - NP-Hard Problem D ...
  • 運算符 1、算數運算: 2、比較運算: 3、賦值運算: 4、邏輯運算: 5、成員運算: 基本數據類型 1、數字 int(整型) 在32位機器上,整數的位數為32位,取值範圍為-2**31~2**31-1,即-2147483648~2147483647 在64位系統上,整數的位數為64位,取值範圍為- ...
  • 1.項目功能展示 (1)註冊 (2)修改地址與級別信息,點擊修改 (3)再添加一位成員,進行刪除 點擊第二行的刪除 (4)登錄模塊測試 輸入資料庫中沒有的信息: 輸入資料庫中存在的信息: 2. Web.xml Spring提供了ContextLoaderListener,該監聽器實現了Servlet ...
  • Lambda表達式 lambda expression是一個匿名函數,Lambda表達式基於數學中的λ演算得名,直接對應於其中的lambda抽象(lambda abstraction),是一個匿名函數,即沒有函數名的函數。表達式使用 Lambda 運算符 =>,該運算符讀為“goes to”。語法如 ...
  • 第一次寫博客,寫的不好休怪哈。 版本1:最簡單的單例模式 方法一: 方法二: 兩點:1)保證所有構造函數不被外部所調用;2)利用屬性或者方法調用對象。 缺點:無法保證線程的安全性,多個線程的情況下可能創建多個對象。 版本2:線程安全的單例模式 缺點:無論對象是否已經被創建,都要進行加鎖,增加了不必要 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...