筆者在寫作本章節的時候,並不敢把此章節的標題叫做《高性能日誌系統的設計》,之所以不敢加上“高性能”三個字的原因是,第一,我的對於日誌系統設計知識和經驗都來自於學習和工作經驗,我並不是原創者,只是知識的搬運工;第二,目前有許多優秀的、被廣泛使用的開源的日誌系統,他們給了我很多啓發,不敢在此班門弄斧。不管怎樣,筆者還是想寫一些自己關於對日誌系統的理解和經驗,讓我們開始吧。
爲什麼需要日誌
實際的軟件項目產出都有一個流程,即先開發、測試,再發布生產,由於人的因素,既然是軟件產品就不可能百分百沒有bug或者邏輯錯誤,對於已經發布了生產的項目,一旦某個時刻產生非預期的結果,我們就需要去定位和排查問題。但是一般正式的生產環境的服務器或者產品是不允許開發人員通過附加調試器去排查問題的,主要有如下可能原因:
- 在一些大的互聯網公司或者部門分工明確的公司,開發部門、測試部分和產品運維部門是分工明確的,軟件產品一旦發佈到生產環境以後,將全部交由運維部門人員去管理和維護,而原來開發此產品的開發人員不再擁有相關的操作程序的權限。
- 對於已經上了生產環境的服務,其數據是公司的核心產值所在,一般不允許或者不敢被開發人員隨意調試或者操作,以免對公司造成損失。
- 發佈到生產環境的服務,一般爲了讓程序執行效率更高、文件體積更小,都是去掉調試符號後的版本,不方便也不利於調試。
既然我們無法通過調試器去調試,這個時候我們爲了跟蹤和回憶當時的程序行爲進而定位問題,我們就需要日誌系統。
退一步說,即使在開發或者測試環境,我們可以把程序附加到調試器上去調試,但是對於一些特定的程序行爲,我們無法通過設置斷點,讓程序在某個時刻暫停下來進行調試。例如,對於某些網絡通信功能,如果暫停時間過長(相對於操作系統的操作來說),通信的對端可能由於彼端沒有在規定時間內響應而斷開連接,導致程序邏輯無法進入我們想要的執行流中去;再例如,對於一些高頻操作(如心跳包、定時器、界面繪製下的某些高頻重複行爲),可能在少量次數下無法觸發我們想要的行爲,而通過斷點的暫停方式,我們不得不重複操作幾十次、上百次甚至更多,這樣排查問題效率是非常低下的。對於這類操作,我們可以通過打印日誌,將當時的程序行爲上下文現場記錄下來,然後從日誌系統中找到某次不正常的行爲的上下文信息。這也是日誌的另外一個作用。
本文將從技術和業務上兩個方面來介紹日誌系統相關的設計與開發,所謂技術上,就是如何從程序開發的角度設計一款功能強大、性能優越、使用方便的日誌系統;而業務上,是指我們在使用日誌系統時,應該去記錄哪些行爲和數據,既簡潔、不囉嗦,又方便需要時準確快速地定位問題。
日誌系統的技術上的實現
日誌的最初的原型即將程序運行的狀態打印出來,對於C/C++這門語言來說,即可以利用printf、std::cout等控制檯輸出函數,將日誌信息輸出到控制檯,這類簡單的情形我們不在此過多贅述。
對於,實際的商業項目,爲了方便排查問題,我們一般不將日誌寫到控制檯,而是輸出到文件或者數據庫系統。不管哪一種,其思路基本上一致,我們這裏以寫文件爲例來詳細介紹。
同步寫日誌
所謂同步寫日誌,指的是在輸出日誌的地方,將日誌即時寫入到文件中去。根據筆者的經驗,這種設計廣泛地用於相當數量的客戶端軟件。筆者曾從事過數年的客戶端開發(包括pc、安卓版本),設計過一些功能複雜的金融客戶端產品,在這些系統中採用的就是這種同步寫日誌的方式。之所以使用這種方式其主要原因就是設計簡單,而又不會影響用戶使用體驗。說到這裏讀者可能有這樣一個疑問:一般的客戶端軟件,一般存在界面,而界面部分所屬的邏輯就是程序的主線程,如果採取這種同步寫日誌的方式,當寫日誌時,寫文件是磁盤IO操作,相比較程序其他部分是CPU操作,前者要慢很多,這樣勢必造成CPU等待,進而導致主線程“卡”在寫文件處,進而造成界面卡頓,從而導致用戶使用軟件的體驗不好。讀者的這種顧慮確實是存在的。但是,很多時候我們不用擔心這種問題,主要有兩個原因:
- 對於客戶端程序,即使在主線程(UI線程)中同步寫文件,其單次或者幾次磁盤操作累加時間,與人(用戶)的可感知時間相比,也是非常小的,也就是說用戶根本感覺不到這種同步寫文件造成的延遲。當然,這裏也給您一個提醒就是,如果在UI線程裏面寫日誌,尤其是在一些高頻操作中(如Windows的界面繪製消息WM_PAINT處理邏輯中),一定要控制寫日誌的長度和次數,否則就會因頻繁寫文件和一次寫入數據過大而對界面造成卡頓。
- 客戶端程序除了UI線程,還有其他與界面無關的工作線程,在這些線程中直接寫文件,一般不會對用戶的體驗產生什麼影響。
說了這麼多,我們給出一個具體的例子。
日誌類的.h文件
1/** 2 *@desc: IULog.h 3 *@author: zhangyl 4 *@date: 2014.12.25 5 */ 6#ifndef __LOG_H__ 7#define __LOG_H__ 8 9enum LOG_LEVEL 10{ 11 LOG_LEVEL_INFO, 12 LOG_LEVEL_WARNING, 13 LOG_LEVEL_ERROR 14}; 15 16//注意:如果打印的日誌信息中有中文,則格式化字符串要用_T()宏包裹起來, 17#define LOG_INFO(...) CIULog::Log(LOG_LEVEL_INFO, __FUNCSIG__,__LINE__, __VA_ARGS__) 18#define LOG_WARNING(...) CIULog::Log(LOG_LEVEL_WARNING, __FUNCSIG__, __LINE__,__VA_ARGS__) 19#define LOG_ERROR(...) CIULog::Log(LOG_LEVEL_ERROR, __FUNCSIG__,__LINE__, __VA_ARGS__) 20 21class CIULog 22{ 23public: 24 static bool Init(bool bToFile, bool bTruncateLongLog, PCTSTR pszLogFileName); 25 static void Uninit(); 26 27 static void SetLevel(LOG_LEVEL nLevel); 28 29 //不輸出線程ID號和所在函數簽名、行號 30 static bool Log(long nLevel, PCTSTR pszFmt, ...); 31 //輸出線程ID號和所在函數簽名、行號 32 static bool Log(long nLevel, PCSTR pszFunctionSig, int nLineNo, PCTSTR pszFmt, ...); //注意:pszFunctionSig參數爲Ansic版本 33 static bool Log(long nLevel, PCSTR pszFunctionSig, int nLineNo, PCSTR pszFmt, ...); 34private: 35 CIULog() = delete; 36 ~CIULog() = delete; 37 38 CIULog(const CIULog& rhs) = delete; 39 CIULog& operator=(const CIULog& rhs) = delete; 40 41 static void GetTime(char* pszTime, int nTimeStrLength); 42 43private: 44 static bool m_bToFile; //日誌寫入文件還是寫到控制檯 45 static HANDLE m_hLogFile; 46 static bool m_bTruncateLongLog; //長日誌是否截斷 47 static LOG_LEVEL m_nLogLevel; //日誌級別 48}; 49 50#endif // !__LOG_H__
日誌的cpp文件(實現文件)
1/** 2 *@desc: IULog.cpp 3 *@author: zhangyl 4 *@date: 2014.12.25 5 */ 6#include "stdafx.h" 7#include "IULog.h" 8#include "EncodingUtil.h" 9#include <tchar.h> 10 11#ifndef LOG_OUTPUT 12#define LOG_OUTPUT 13#endif 14 15#define MAX_LINE_LENGTH 256 16 17bool CIULog::m_bToFile = false; 18bool CIULog::m_bTruncateLongLog = false; 19HANDLE CIULog::m_hLogFile = INVALID_HANDLE_VALUE; 20LOG_LEVEL CIULog::m_nLogLevel = LOG_LEVEL_INFO; 21 22bool CIULog::Init(bool bToFile, bool bTruncateLongLog, PCTSTR pszLogFileName) 23{ 24#ifdef LOG_OUTPUT 25 m_bToFile = bToFile; 26 m_bTruncateLongLog = bTruncateLongLog; 27 28 if (pszLogFileName == NULL || pszLogFileName[0] == NULL) 29 return FALSE; 30 31 TCHAR szHomePath[MAX_PATH] = {0}; 32 ::GetModuleFileName(NULL, szHomePath, MAX_PATH); 33 for (int i = _tcslen(szHomePath); i >= 0; --i) 34 { 35 if (szHomePath[i] == _T('\\')) 36 { 37 szHomePath[i] = _T('\0'); 38 break; 39 } 40 } 41 42 TCHAR szLogDirectory[MAX_PATH] = { 0 }; 43 _stprintf_s(szLogDirectory, _T("%s\\Logs\\"), szHomePath); 44 45 DWORD dwAttr = ::GetFileAttributes(szLogDirectory); 46 if (!((dwAttr != 0xFFFFFFFF) && (dwAttr & FILE_ATTRIBUTE_DIRECTORY))) 47 { 48 TCHAR cPath[MAX_PATH] = { 0 }; 49 TCHAR cTmpPath[MAX_PATH] = { 0 }; 50 TCHAR* lpPos = NULL; 51 TCHAR cTmp = _T('\0'); 52 53 _tcsncpy_s(cPath, szLogDirectory, MAX_PATH); 54 55 for (int i = 0; i < (int)_tcslen(cPath); i++) 56 { 57 if (_T('\\') == cPath[i]) 58 cPath[i] = _T('/'); 59 } 60 61 lpPos = _tcschr(cPath, _T('/')); 62 while (lpPos != NULL) 63 { 64 if (lpPos == cPath) 65 { 66 lpPos++; 67 } 68 else 69 { 70 cTmp = *lpPos; 71 *lpPos = _T('\0'); 72 _tcsncpy_s(cTmpPath, cPath, MAX_PATH); 73 ::CreateDirectory(cTmpPath, NULL); 74 *lpPos = cTmp; 75 lpPos++; 76 } 77 lpPos = _tcschr(lpPos, _T('/')); 78 } 79 } 80 81 m_hLogFile = ::CreateFile(pszLogFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL); 82 if (m_hLogFile == INVALID_HANDLE_VALUE) 83 return false; 84 85#endif // end LOG_OUTPUT 86 87 return true; 88} 89 90void CIULog::Uninit() 91{ 92#ifdef LOG_OUTPUT 93 if(m_hLogFile != INVALID_HANDLE_VALUE) 94 { 95 ::CloseHandle(m_hLogFile); 96 m_hLogFile = INVALID_HANDLE_VALUE; 97 } 98#endif //end LOG_OUTPUT 99} 100 101void CIULog::SetLevel(LOG_LEVEL nLevel) 102{ 103 m_nLogLevel = nLevel; 104} 105 106bool CIULog::Log(long nLevel, PCTSTR pszFmt, ...) 107{ 108#ifdef LOG_OUTPUT 109 if (nLevel < m_nLogLevel) 110 return false; 111 112 char szTime[64] = { 0 }; 113 GetTime(szTime,ARRAYSIZE(szTime)); 114 std::string strDebugInfo(szTime); 115 116 std::string strLevel("[INFO]"); 117 if (nLevel == LOG_LEVEL_WARNING) 118 strLevel = "[Warning]"; 119 else if (nLevel == LOG_LEVEL_ERROR) 120 strLevel = "[Error]"; 121 122 strDebugInfo += strLevel; 123 124 //當前線程信息 125 char szThreadID[32] = { 0 }; 126 DWORD dwThreadID = ::GetCurrentThreadId(); 127 sprintf_s(szThreadID, ARRAYSIZE(szThreadID), "[ThreadID: %u]", dwThreadID); 128 strDebugInfo += szThreadID; 129 130 //log正文 131 std::wstring strLogMsg; 132 va_list ap; 133 va_start(ap, pszFmt); 134 int nLogMsgLength = _vsctprintf(pszFmt, ap); 135 //容量必須算上最後一個\0 136 if ((int)strLogMsg.capacity() < nLogMsgLength + 1) 137 { 138 strLogMsg.resize(nLogMsgLength + 1); 139 } 140 _vstprintf_s((TCHAR*)strLogMsg.data(), strLogMsg.capacity(), pszFmt, ap); 141 va_end(ap); 142 143 //string內容正確但length不對,恢復一下其length 144 std::wstring strMsgFormal; 145 strMsgFormal.append(strLogMsg.c_str(), nLogMsgLength); 146 147 //如果日誌開啓截斷,長日誌只取前MAX_LINE_LENGTH個字符 148 if (m_bTruncateLongLog) 149 strMsgFormal = strMsgFormal.substr(0, MAX_LINE_LENGTH); 150 151 std::string strLogMsgAscii; 152 strLogMsgAscii = EncodeUtil::UnicodeToAnsi(strMsgFormal); 153 154 strDebugInfo += strLogMsgAscii; 155 strDebugInfo += "\r\n"; 156 157 if(m_bToFile) 158 { 159 if(m_hLogFile == INVALID_HANDLE_VALUE) 160 return false; 161 162 ::SetFilePointer(m_hLogFile, 0, NULL, FILE_END); 163 DWORD dwBytesWritten = 0; 164 ::WriteFile(m_hLogFile, strDebugInfo.c_str(), strDebugInfo.length(), &dwBytesWritten, NULL); 165 ::FlushFileBuffers(m_hLogFile); 166 return true; 167 } 168 169 ::OutputDebugStringA(strDebugInfo.c_str()); 170 171#endif // end LOG_OUTPUT 172 173 return true; 174} 175 176bool CIULog::Log(long nLevel, PCSTR pszFunctionSig, int nLineNo, PCTSTR pszFmt, ...) 177{ 178#ifdef LOG_OUTPUT 179 if (nLevel < m_nLogLevel) 180 return false; 181 182 //時間 183 char szTime[64] = { 0 }; 184 GetTime(szTime, ARRAYSIZE(szTime)); 185 std::string strDebugInfo(szTime); 186 187 //錯誤級別 188 std::string strLevel("[INFO]"); 189 if (nLevel == LOG_LEVEL_WARNING) 190 strLevel = "[Warning]"; 191 else if (nLevel == LOG_LEVEL_ERROR) 192 strLevel = "[Error]"; 193 194 strDebugInfo += strLevel; 195 196 //當前線程信息 197 char szThreadID[32] = {0}; 198 DWORD dwThreadID = ::GetCurrentThreadId(); 199 sprintf_s(szThreadID, ARRAYSIZE(szThreadID), "[ThreadID: %u]", dwThreadID); 200 strDebugInfo += szThreadID; 201 202 //函數簽名 203 char szFuncSig[512] = { 0 }; 204 sprintf_s(szFuncSig, "[%s:%d]", pszFunctionSig, nLineNo); 205 strDebugInfo += szFuncSig; 206 207 //log正文 208 std::wstring strLogMsg; 209 va_list ap; 210 va_start(ap, pszFmt); 211 int nLogMsgLength = _vsctprintf(pszFmt, ap); 212 //容量必須算上最後一個\0 213 if ((int)strLogMsg.capacity() < nLogMsgLength + 1) 214 { 215 strLogMsg.resize(nLogMsgLength + 1); 216 } 217 _vstprintf_s((TCHAR*)strLogMsg.data(), strLogMsg.capacity(), pszFmt, ap); 218 va_end(ap); 219 220 //string內容正確但length不對,恢復一下其length 221 std::wstring strMsgFormal; 222 strMsgFormal.append(strLogMsg.c_str(), nLogMsgLength); 223 224 //如果日誌開啓截斷,長日誌只取前MAX_LINE_LENGTH個字符 225 if (m_bTruncateLongLog) 226 strMsgFormal = strMsgFormal.substr(0, MAX_LINE_LENGTH); 227 228 std::string strLogMsgAscii; 229 strLogMsgAscii = EncodeUtil::UnicodeToAnsi(strMsgFormal); 230 231 strDebugInfo += strLogMsgAscii; 232 strDebugInfo += "\r\n"; 233 234 if(m_bToFile) 235 { 236 if(m_hLogFile == INVALID_HANDLE_VALUE) 237 return false; 238 239 ::SetFilePointer(m_hLogFile, 0, NULL, FILE_END); 240 DWORD dwBytesWritten = 0; 241 ::WriteFile(m_hLogFile, strDebugInfo.c_str(), strDebugInfo.length(), &dwBytesWritten, NULL); 242 ::FlushFileBuffers(m_hLogFile); 243 return true; 244 } 245 246 ::OutputDebugStringA(strDebugInfo.c_str()); 247 248#endif // end LOG_OUTPUT 249 250 return true; 251} 252 253bool CIULog::Log(long nLevel, PCSTR pszFunctionSig, int nLineNo, PCSTR pszFmt, ...) 254{ 255#ifdef LOG_OUTPUT 256 if (nLevel < m_nLogLevel) 257 return false; 258 259 //時間 260 char szTime[64] = { 0 }; 261 GetTime(szTime, ARRAYSIZE(szTime)); 262 std::string strDebugInfo(szTime); 263 264 //錯誤級別 265 std::string strLevel("[INFO]"); 266 if (nLevel == LOG_LEVEL_WARNING) 267 strLevel = "[Warning]"; 268 else if (nLevel == LOG_LEVEL_ERROR) 269 strLevel = "[Error]"; 270 271 strDebugInfo += strLevel; 272 273 //當前線程信息 274 char szThreadID[32] = {0}; 275 DWORD dwThreadID = ::GetCurrentThreadId(); 276 sprintf_s(szThreadID, ARRAYSIZE(szThreadID), "[ThreadID: %u]", dwThreadID); 277 strDebugInfo += szThreadID; 278 279 //函數簽名 280 char szFuncSig[512] = { 0 }; 281 sprintf_s(szFuncSig, "[%s:%d]", pszFunctionSig, nLineNo); 282 strDebugInfo += szFuncSig; 283 284 //日誌正文 285 std::string strLogMsg; 286 va_list ap; 287 va_start(ap, pszFmt); 288 int nLogMsgLength = _vscprintf(pszFmt, ap); 289 //容量必須算上最後一個\0 290 if ((int)strLogMsg.capacity() < nLogMsgLength + 1) 291 { 292 strLogMsg.resize(nLogMsgLength + 1); 293 } 294 vsprintf_s((char*)strLogMsg.data(), strLogMsg.capacity(), pszFmt, ap); 295 va_end(ap); 296 297 //string內容正確但length不對,恢復一下其length 298 std::string strMsgFormal; 299 strMsgFormal.append(strLogMsg.c_str(), nLogMsgLength); 300 301 //如果日誌開啓截斷,長日誌只取前MAX_LINE_LENGTH個字符 302 if (m_bTruncateLongLog) 303 strMsgFormal = strMsgFormal.substr(0, MAX_LINE_LENGTH); 304 305 strDebugInfo += strMsgFormal; 306 strDebugInfo += "\r\n"; 307 308 if(m_bToFile) 309 { 310 if(m_hLogFile == INVALID_HANDLE_VALUE) 311 return false; 312 313 ::SetFilePointer(m_hLogFile, 0, NULL, FILE_END); 314 DWORD dwBytesWritten = 0; 315 ::WriteFile(m_hLogFile, strDebugInfo.c_str(), strDebugInfo.length(), &dwBytesWritten, NULL); 316 ::FlushFileBuffers(m_hLogFile); 317 return true; 318 } 319 320 ::OutputDebugStringA(strDebugInfo.c_str()); 321 322#endif // end LOG_OUTPUT 323 324 return true; 325} 326 327void CIULog::GetTime(char* pszTime, int nTimeStrLength) 328{ 329 SYSTEMTIME st = {0}; 330 ::GetLocalTime(&st); 331 sprintf_s(pszTime, nTimeStrLength, "[%04d-%02d-%02d %02d:%02d:%02d:%04d]", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); 332}
上述代碼中根據日誌級別定義了三個宏LOG_INFO、LOG_WARNING、LOG_ERROR,如果要使用該日誌模塊,只需要在程序啓動處的地方調用CIULog::Init函數初始化日誌:
1SYSTEMTIME st = {0}; 2::GetLocalTime(&st); 3TCHAR szLogFileName[MAX_PATH] = {0}; 4_stprintf_s(szLogFileName, MAX_PATH, _T("%s\\Logs\\%04d%02d%02d%02d%02d%02d.log"), g_szHomePath, st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond); 5CIULog::Init(true, false, szLogFileName);
當然,最佳的做法,在程序退出的地方,調用CIULog::Uninit回收日誌模塊相關的資源:
1CIULog::Uninit();
在做好這些準備工作以後,如果你想在程序的某個地方寫一條日誌,只需要這樣寫:
1LOG_INFO("Request logon: Account=%s, Password=*****, Status=%d, LoginType=%d.", pLoginRequest->m_szAccountName, pLoginRequest->m_szPassword, pLoginRequest->m_nStatus, (long)pLoginRequest->m_nLoginType); 2LOG_WARN("Some warning..."); 3LOG_ERROR("Recv data error, errorNO=%d.", ::WSAGetLastError());
關於CIULog這個日誌模塊類,如果讀者要想實際運行查看效果,可以從鏈接(https://github.com/baloonwj/flamingo/tree/master/flamingoclient)下載完整的項目代碼來運行。該日誌輸出效果如下:
1[2018-11-09 23:52:54:0826][INFO][ThreadID: 7252][bool __thiscall CIUSocket::Login(const char *,const char *,int,int,int,class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > &):1107]Request logon: Account=zhangy, Password=*****, Status=76283204, LoginType=1. 2[2018-11-09 23:52:56:0352][INFO][ThreadID: 5828][void __thiscall CIUSocket::SendThreadProc(void):794]Recv data thread start... 3[2018-11-09 23:52:56:0385][INFO][ThreadID: 6032][void __thiscall CSendMsgThread::HandleUserBasicInfo(const class CUserBasicInfoRequest *):298]Request to get userinfo. 4[2018-11-09 23:52:56:0355][INFO][ThreadID: 7140][void __thiscall CIUSocket::RecvThreadProc(void):842]Recv data thread start... 5[2018-11-09 23:52:57:0254][INFO][ThreadID: 7220][int __thiscall CRecvMsgThread::HandleFriendListInfo(const class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > &):593]Recv user basic info, info count=1: 6[2018-11-09 23:52:57:0685][INFO][ThreadID: 6032][void __thiscall CSendMsgThread::HandleGroupBasicInfo(const class CGroupBasicInfoRequest *):336]Request to get group members, groupid=268435457. 7[2018-11-09 23:52:57:0809][INFO][ThreadID: 8184][bool __thiscall CIUSocket::ConnectToImgServer(int):428]Connect to img server:120.55.94.78, port:20002 successfully. 8[2018-11-09 23:52:58:0048][INFO][ThreadID: 6032][void __thiscall CSendMsgThread::HandleGroupBasicInfo(const class CGroupBasicInfoRequest *):336]Request to get group members, groupid=268435458. 9[2018-11-09 23:52:58:0084][INFO][ThreadID: 7220][int __thiscall CRecvMsgThread::HandleGroupBasicInfo(const class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > &):671]Recv group member info, groupid=268435457, info count=29: 10[2018-11-09 23:52:58:0500][Error][ThreadID: 8184][long __thiscall CImageTaskThread::DownloadImage(const char *,const wchar_t *,int,struct HWND__ *,void *):568]Download img failed: read filesize error, filesize=0, img name=be19574dcdd11fb9a96cf00f7e5f0e66 11[2018-11-09 23:52:58:0943][INFO][ThreadID: 6032][void __thiscall CSendMsgThread::HandleGroupBasicInfo(const class CGroupBasicInfoRequest *):336]Request to get group members, groupid=268435485. 12[2018-11-09 23:52:59:0310][INFO][ThreadID: 7220][int __thiscall CRecvMsgThread::HandleGroupBasicInfo(const class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> > &):671]Recv group member info, groupid=268435458, info count=7: 13[2018-11-09 23:52:59:0508][Error][ThreadID: 8184][long __thiscall CImageTaskThread::DownloadImage(const char *,const wchar_t *,int,struct HWND__ *,void *):619]Failed to download image: F:\mycode\flamingoX\flamingoclient\Source\..\Bin\Users\zhangy\UserThumb\15.png.
多線程同步寫日誌出現的問題一
從上面的日誌輸出來看,這種同步的日誌輸出方式,也存在時間順序不正確的問題(時間戳大的日誌比時間戳小的日誌靠前)。這是由於多線程同時寫日誌到同一個文件時,產生日誌的時間和實際寫入磁盤的時間不是一個原子操作。下圖解釋了該現象出現的根源:
多線程寫同一個日誌文件出現先產生的日誌後寫入到文件中的現象
好在這種時間順序不正確只會出現在不同線程之間,對於同一個線程的不同時間的日誌記錄順序肯定是正確的。所以這種日期錯亂現象,並不影響我們使用日誌。
多線程同步寫日誌出現的問題二
多線程同時寫入同一個日誌文件還有一個問題,就是假設線程A寫某一個時刻追加日誌內容爲“AAAAA”,線程B在同一時刻追加日誌內容爲“BBBBB”,線程C在同一時刻追加日誌內容爲“CCCCC”,那麼最終的日誌文件中的內容會不會出現“AABBCCABCAACCBB”這種格式?
在類Unix系統上(包括linux),同一個進程內針對同一個FILE*的操作是線程安全的,也就是說,在這類操作系統上得到的日誌結果A、B、C各個字母組一定是連續在一起,也就是說最終得到的日誌內容可能是“AAAAACCCCCBBBBB”或“AAAAABBBBBCCCCC”等這種連續的格式,絕不會出現A、B、C字母相間的現象。
而在Windows系統上,對於FILE*的操作並不是線程安全的。但是筆者做了大量實驗,在Windows系統上也沒有出現這種A、B、C字母相間的現象。(關於這個問題的討論,可以參考這裏:https://www.zhihu.com/question/40472431)
加上這種同步日誌的實現方式,一般用於低頻寫日誌的軟件系統中(如客戶端軟件),所以,我們可以認爲這種多線程同時寫日誌到一個文件中是可行的。