企業級應用程式需要結構化、高效能、線程安全的日誌記錄基礎設施。本文呈現一個基於 MFC 的日誌記錄 DLL 設計,核心特色包括:模組註冊機制、每模組專用寫入執行緒、非同步訊息佇列、檔案互斥存取與靈活的除錯/正式版切換策略。
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);
};
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;
}
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;
}
// 全域日誌記錄器實例 (建議放在 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 編碼寫入 |
LoadLibrary/GetProcAddress 動態載入,降低啟動依賴本設計展示了「生產者-消費者」模式在日誌系統中的經典應用:前端 CInfoReport 提供簡潔同步介面,後端 CWriteInfo 執行緒負責序列化寫入與資源管理。雖然存在目錄管理、DLL 全域實例等待解決問題,但核心架構清晰、擴充性強,適合作為企業級日誌基礎設施的起點。現代化重構建議引入 spdlog、log4cplus 或自行實作基於環狀緩衝區的無鎖日誌系統。