首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> VC >> 關於VC++

多執行緒日誌記錄 DLL 設計:模組化註冊、專用寫入緒與互斥機制

     編輯:關於VC++

企業級應用程式需要結構化、高效能、線程安全的日誌記錄基礎設施。本文呈現一個基於 MFC 的日誌記錄 DLL 設計,核心特色包括:模組註冊機制、每模組專用寫入執行緒、非同步訊息佇列、檔案互斥存取與靈活的除錯/正式版切換策略。

核心架構設計

1. 匯出介面類別:CInfoReport


class AFX_EXT_CLASS CInfoReport : public CObject
{
public:
    // 靜態執行緒指標鏈表,全模組共享
    static CPtrList m_lstpThreads;

    // 註冊模組,返回模組 ID(用於後續寫入)
    DWORD Register(LPCTSTR lpszModuleName);
    
    // 寫入日誌(簡易版)
    void WriteInfo(DWORD dwModuleID, const CString& strInfo);
    
    // 寫入日誌(詳細版:來源+描述)
    void WriteInfo(DWORD dwModuleID, const CString& strSource, const CString& strInfo);
};

2. 背景寫入執行緒類別:CWriteInfo


class CWriteInfo : public CWinThread
{
private:
    CString m_strModuleName;    // 模組名稱 = 日誌檔案名(不含副檔名)
    CFile   m_file;             // 日誌檔案物件
    HANDLE  m_hEventBusy;       // 檔案互斥事件

public:
    CString GetName() const;
    void SetModuleFileName(const CString& strFileName);

protected:
    // 訊息處理:接收 CInfoReport::WriteInfo 發送的 TM_WRITE_INFO
    afx_msg void OnWriteInfo(WPARAM wParam, LPARAM lParam);
    
    DECLARE_MESSAGE_MAP()
};

關鍵實作細節

模組註冊與執行緒建立


DWORD CInfoReport::Register(LPCTSTR lpszModuleName)
{
    // 1. 建立專屬寫入執行緒
    CWriteInfo* pThread = (CWriteInfo*)AfxBeginThread(
        RUNTIME_CLASS(CWriteInfo), 
        THREAD_PRIORITY_NORMAL, 
        0, 
        CREATE_SUSPENDED
    );
    
    if (!pThread) return 0;  // 失敗返回 0 (保留給 FatalErr.log)
    
    // 2. 設定模組名稱,啟動執行緒
    pThread->SetModuleFileName(lpszModuleName);
    pThread->m_bAutoDelete = FALSE;  // 手動管理生命週期
    pThread->ResumeThread();
    
    // 3. 加入靜態鏈表管理
    m_lstpThreads.AddTail(pThread);
    
    // 4. 返回執行緒指標作為 ModuleID (非零)
    return (DWORD)pThread;
}

非同步日誌寫入:PostThreadMessage 機制


void CInfoReport::WriteInfo(DWORD dwModuleID, const CString& strInfo)
{
    if (dwModuleID == 0)
    {
        // 特殊處理:寫入 FatalErr.log (同步寫入或獨立機制)
        WriteToFatalLog(strInfo);
        return;
    }
    
    CWriteInfo* pThread = (CWriteInfo*)dwModuleID;
    
    // 配置訊息參數 (需動態配置字串緩衝區,執行緒接收後釋放)
    CString* pStr = new CString(strInfo);
    pThread->PostThreadMessage(WM_USER_WRITE_INFO, 0, (LPARAM)pStr);
}

void CInfoReport::WriteInfo(DWORD dwModuleID, const CString& strSource, const CString& strInfo)
{
    CString combined;
    combined.Format(_T("[%s] %s"), strSource, strInfo);
    WriteInfo(dwModuleID, combined);
}

執行緒端訊息處理與檔案互斥


BEGIN_MESSAGE_MAP(CWriteInfo, CWinThread)
    ON_THREAD_MESSAGE(WM_USER_WRITE_INFO, OnWriteInfo)
END_MESSAGE_MAP()

void CWriteInfo::OnWriteInfo(WPARAM wParam, LPARAM lParam)
{
    CString* pStr = (CString*)lParam;
    if (!pStr) return;
    
    // 1. 等待檔案互斥事件 (自動重置事件)
    WaitForSingleObject(m_hEventBusy, INFINITE);
    
    // 2. 確保檔案已開啟 (延遲開啟策略)
    if (m_file.m_hFile == CFile::hFileNull)
    {
        CString strPath = GetLogFilePath();  // 如: .\Log\ModuleName.log
        if (!m_file.Open(strPath, CFile::modeCreate | CFile::modeNoTruncate | CFile::modeWrite | CFile::shareDenyNone))
        {
            SetEvent(m_hEventBusy);
            delete pStr;
            return;
        }
        m_file.SeekToEnd();  // 追加模式
    }
    
    // 3. 寫入時間戳記 + 內容
    CString strLog;
    CTime timeNow = CTime::GetCurrentTime();
    strLog.Format(_T("[%s] %s\r\n"), timeNow.Format(_T("%Y-%m-%d %H:%M:%S")), *pStr);
    
    m_file.Write(strLog, strLog.GetLength() * sizeof(TCHAR));
    m_file.Flush();
    
    // 4. 釋放互斥、清理
    SetEvent(m_hEventBusy);
    delete pStr;
}

使用範例:SDI 應用程式整合


// 全域日誌記錄器實例 (建議放在 CWinApp 派生類別中)
CInfoReport g_LogReporter;

// 模組 ID 快取
DWORD g_dwViewModuleID = 0;

void CMyView::OnInitialUpdate()
{
    CView::OnInitialUpdate();
    
    // 註冊模組 (僅執行一次)
    if (g_dwViewModuleID == 0)
        g_dwViewModuleID = g_LogReporter.Register(_T("MyView"));
}

void CMyView::OnDraw(CDC* pDC)
{
    // 測試:每次重繪記錄日誌
    if (g_dwViewModuleID != 0)
    {
        CString strInfo;
        strInfo.Format(_T("OnDraw called, DC=0x%08X"), pDC->m_hDC);
        g_LogReporter.WriteInfo(g_dwViewModuleID, _T("OnDraw"), strInfo);
    }
    
    // 實際繪圖邏輯...
}

已知限制與改進方向

問題現狀改進建議
Log 目錄不存在程式異常終止註冊時自動建立目錄 SHCreateDirectoryEx
DLL 中全域實例失效全域 CInfoReport 在 DLL 中導致 WaitForSingleObject 卡死改為類別成員變數或延遲初始化 (InitInstance 中註冊)
日誌輪轉無限制增長依日期/大小分割,實作 Rolling File Appender
效能瓶頸高頻寫入阻塞 UI 執行緒批次寫入、非同步 I/O (IOCP)、記憶體緩衝區
Unicode/ANSI 相容僅支援專案字元集統一使用 UTF-8 編碼寫入

生產環境部署建議

  1. 建置配置區分:Debug 版啟用詳細日誌,Release 版僅記錄關鍵事件
  2. 靜態連結 MFC:避免 MFC DLL 版本衝突,/MTd 或 /MT 編譯選項
  3. 延遲載入 DLL:使用 LoadLibrary/GetProcAddress 動態載入,降低啟動依賴
  4. 結構化日誌格式:採用 JSON Lines 或 CSV 格式,便於後續分析工具解析
  5. 遠端收集支援:擴充 UDP/HTTP 發送器,整合 ELK/Grafana/Loki 等觀測平台

總結

本設計展示了「生產者-消費者」模式在日誌系統中的經典應用:前端 CInfoReport 提供簡潔同步介面,後端 CWriteInfo 執行緒負責序列化寫入與資源管理。雖然存在目錄管理、DLL 全域實例等待解決問題,但核心架構清晰、擴充性強,適合作為企業級日誌基礎設施的起點。現代化重構建議引入 spdlog、log4cplus 或自行實作基於環狀緩衝區的無鎖日誌系統。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved