C++日誌系統如何設計

筆者在寫作本章節的時候,並不敢把此章節的標題叫做《高性能日誌系統的設計》,之所以不敢加上“高性能”三個字的原因是,第一,我的對於日誌系統設計知識和經驗都來自於學習和工作經驗,我並不是原創者,只是知識的搬運工;第二,目前有許多優秀的、被廣泛使用的開源的日誌系統,他們給了我很多啓發,不敢在此班門弄斧。不管怎樣,筆者還是想寫一些自己關於對日誌系統的理解和經驗,讓我們開始吧。

爲什麼需要日誌

實際的軟件項目產出都有一個流程,即先開發、測試,再發布生產,由於人的因素,既然是軟件產品就不可能百分百沒有bug或者邏輯錯誤,對於已經發布了生產的項目,一旦某個時刻產生非預期的結果,我們就需要去定位和排查問題。但是一般正式的生產環境的服務器或者產品是不允許開發人員通過附加調試器去排查問題的,主要有如下可能原因:

  1. 在一些大的互聯網公司或者部門分工明確的公司,開發部門、測試部分和產品運維部門是分工明確的,軟件產品一旦發佈到生產環境以後,將全部交由運維部門人員去管理和維護,而原來開發此產品的開發人員不再擁有相關的操作程序的權限。
  2. 對於已經上了生產環境的服務,其數據是公司的核心產值所在,一般不允許或者不敢被開發人員隨意調試或者操作,以免對公司造成損失。
  3. 發佈到生產環境的服務,一般爲了讓程序執行效率更高、文件體積更小,都是去掉調試符號後的版本,不方便也不利於調試。

既然我們無法通過調試器去調試,這個時候我們爲了跟蹤和回憶當時的程序行爲進而定位問題,我們就需要日誌系統。

退一步說,即使在開發或者測試環境,我們可以把程序附加到調試器上去調試,但是對於一些特定的程序行爲,我們無法通過設置斷點,讓程序在某個時刻暫停下來進行調試。例如,對於某些網絡通信功能,如果暫停時間過長(相對於操作系統的操作來說),通信的對端可能由於彼端沒有在規定時間內響應而斷開連接,導致程序邏輯無法進入我們想要的執行流中去;再例如,對於一些高頻操作(如心跳包、定時器、界面繪製下的某些高頻重複行爲),可能在少量次數下無法觸發我們想要的行爲,而通過斷點的暫停方式,我們不得不重複操作幾十次、上百次甚至更多,這樣排查問題效率是非常低下的。對於這類操作,我們可以通過打印日誌,將當時的程序行爲上下文現場記錄下來,然後從日誌系統中找到某次不正常的行爲的上下文信息。這也是日誌的另外一個作用。

本文將從技術和業務上兩個方面來介紹日誌系統相關的設計與開發,所謂技術上,就是如何從程序開發的角度設計一款功能強大、性能優越、使用方便的日誌系統;而業務上,是指我們在使用日誌系統時,應該去記錄哪些行爲和數據,既簡潔、不囉嗦,又方便需要時準確快速地定位問題。

日誌系統的技術上的實現

日誌的最初的原型即將程序運行的狀態打印出來,對於C/C++這門語言來說,即可以利用printfstd::cout等控制檯輸出函數,將日誌信息輸出到控制檯,這類簡單的情形我們不在此過多贅述。

對於,實際的商業項目,爲了方便排查問題,我們一般不將日誌寫到控制檯,而是輸出到文件或者數據庫系統。不管哪一種,其思路基本上一致,我們這裏以寫文件爲例來詳細介紹。

同步寫日誌

所謂同步寫日誌,指的是在輸出日誌的地方,將日誌即時寫入到文件中去。根據筆者的經驗,這種設計廣泛地用於相當數量的客戶端軟件。筆者曾從事過數年的客戶端開發(包括pc、安卓版本),設計過一些功能複雜的金融客戶端產品,在這些系統中採用的就是這種同步寫日誌的方式。之所以使用這種方式其主要原因就是設計簡單,而又不會影響用戶使用體驗。說到這裏讀者可能有這樣一個疑問:一般的客戶端軟件,一般存在界面,而界面部分所屬的邏輯就是程序的主線程,如果採取這種同步寫日誌的方式,當寫日誌時,寫文件是磁盤IO操作,相比較程序其他部分是CPU操作,前者要慢很多,這樣勢必造成CPU等待,進而導致主線程“卡”在寫文件處,進而造成界面卡頓,從而導致用戶使用軟件的體驗不好。讀者的這種顧慮確實是存在的。但是,很多時候我們不用擔心這種問題,主要有兩個原因:

  1. 對於客戶端程序,即使在主線程(UI線程)中同步寫文件,其單次或者幾次磁盤操作累加時間,與人(用戶)的可感知時間相比,也是非常小的,也就是說用戶根本感覺不到這種同步寫文件造成的延遲。當然,這裏也給您一個提醒就是,如果在UI線程裏面寫日誌,尤其是在一些高頻操作中(如Windows的界面繪製消息WM_PAINT處理邏輯中),一定要控制寫日誌的長度和次數,否則就會因頻繁寫文件和一次寫入數據過大而對界面造成卡頓。
  2. 客戶端程序除了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_INFOLOG_WARNINGLOG_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

加上這種同步日誌的實現方式,一般用於低頻寫日誌的軟件系統中(如客戶端軟件),所以,我們可以認爲這種多線程同時寫日誌到一個文件中是可行的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章