为何要打印⽇志?C++在⾼并发下如何写⽇志⽂件(附源码)?为何要打印⽇志?让程序裸奔不是⼀件很快乐的事么?
有些BUG就像薛定谔的猫,具有波粒⼆象性,当你试图去观察它时它就消失了,当你不去观察它时,它⼜会出现。当你在测试⼈员⾯前赌咒发誓,亲⾃路演把程序跑⼀遍的时候,这些bug就会神奇的消失;⼀旦离开你的骚操作重新回到测试⼈员⼿中,这些bug⼜会突然的出现。这就是开发⼈员和测试⼈员互相对天发誓⾃证清⽩的尬聊场⾯。
因为这些幽灵Bug破坏了团队的氛围,伤害了开发和测试⼈员的感情,从⽽导致了产品质量下降。因此⾮常有必要到这些bug发⽣的原因。
试图到这些具备量⼦特性的bug发⽣的原因,通常的⼿段就是打印⽇志,因为你很难通过⼏次思想实验或⼏次尬聊就能⾃证清⽩。
打印⽇志是查、分析、定位问题的⼀个⾮常有效的⼿段。尤其对后台程序⽽⾔更加重要,后台程序需要长期运⾏,运⾏期间会⾯临各种各样的情况,这些情况包括正常业务逻辑,⾮正常业务逻辑,甚⾄是异常情况。我们往往需要在不同情况下将这些发⽣的事情按时间轴记录下来,从⽽在系统出现问题或瓶颈时进⾏回溯分析。
Java中有现成的⽇志打印类log4j,并将⽇志定义了4个级别:ERROR、WARN、INFO、DEBUG。通常级别越⾼输出的⽇志越少,如果你的后台程序⼀天就能输出⼀个G的ERROR⽇志,那也是⾮常不容易的事,基本可以告别程序员⽣涯了,还打啥⽇志啊?还能让测试⼈员愉快的⼯作么?
通常在出现ERROR⽇志时,我们就需要定位问题,这就需要打印⼤量的DEBUG或INFO⽇志来进⾏问题回溯。这篇⽂章不是介绍Java如何打印⽇志,⽹络上的⽂章已经很多了。
这⾥说⼀下C++在⾼并发下如何打印⽇志,这⾥实现最基本功能(仅做抛砖引⽟之⽤)。通常我们会写⼀个⽇志输出函数,如下:
void Logout(const char* pszLogFile, const char* pszLog)
{
// 参数检查
assert(pszLogFile);
assert(pszLog);
if(NULL == pszLogFile || NULL == pszLog)
return;
// 打开⽇志⽂件
FILE* pFile = fopen(pszLogFile, "ab+");
if(pFile)
{
// 写⽇志
fwrite((void*)pszLog, 1, strlen(pszLog), pFile);
fclose(pFile);
}
}
如果是不同级别的⽇志,可以定义不同⽇志⽂件名,你也可以丰富上⾯的⽇志函数,增加发⽣的时间、所在线程ID、所在⽂件名等辅助信息。
通常情况下这种⽅式没有问题,通过⼀次⽂件IO操作将信息记录下来。但如果是⼀个后台服务,当⾼并发发⽣的时候,问题就会出现了。此时将会有多个线程同时写⽇志的情况发⽣,尤其是那些INFO类型的⽇志,⽐如记录HTTP请求的request信息,这时你的⽇志将会成为系统的瓶颈。毕竟⼤量的⽂件IO操作也是⼀种负担,这些IO操作可能要超过你的正常业务逻辑,⽐如写数据库、⽹络IO等。
此时我们需要将⽇志先写⼊内存块中,当内存写满后在⼀次性Flush到磁盘中,这样就避免了⼤量的磁盘IO操作。下⾯是CFileMem封装类,很简单,⾥⾯注释的很详细就不解释了。
头⽂件.h
#include <windows.h> // for CRITICAL_SECTION
// 缺省内存块⼤⼩,⼤于该值时进⾏将缓存数据写⼊磁盘⽂件
#define SIZE_DEFAULT_MEM (1024*64)  // 64K
class CFileMem
{
public:
CFileMem();
virtual ~CFileMem();
public:
// 打开内存⽂件,dwMemBlockSize为缓存块⼤⼩,⼤于该值时进⾏⼀次IO操作
BOOL Open(const char* pszPathFile, DWORD dwMemBlockSize = SIZE_DEFAULT_MEM);
/*
写数据到内存⽂件,dwFileSize将返回当前磁盘⽂件的⼤⼩,
该值可⽤于外部判断⽇志⽂件是否过⼤,⽐如当dwFileSize⼤于多少M时,可重命名⽂件
从⽽避免单个⽇志⽂件过⼤
*/
BOOL Write(const char* pszLog, DWORD& dwFileSize);
BOOL Write(const unsigned char* pData, DWORD dwDataSize, DWORD& dwFileSize);
// 将内存数据写⼊磁盘
BOOL Flush(DWORD& dwFileSize);
// 关闭内存⽂件
void Close();
// 重命名⽂件
void Rename(const char* pszOldPathFile, const char* pszNewPathFile);
protected:
CRITICAL_SECTION m_csMem;  // 内存池锁
char  m_szPathFile[256];  // ⽇志⽂件路径名
char* m_pMemBlock;    // 内存块,⽤于存储⽂件数据
DWORD m_dwMemBlockSize;  // 内存块⼤⼩
DWORD m_dwPos;    // 内存块的当前位置
};
实现⽂件.cpp
log4j2不打印日志#include "FileMem.h"
#include <stdio.h>
CFileMem::CFileMem()
{
m_pMemBlock = NULL;
m_dwMemBlockSize = 0;
m_dwPos = 0;
memset(m_szPathFile, 0, 256);
::InitializeCriticalSection(&m_csMem);
}
CFileMem::~CFileMem()
{
Close();
::DeleteCriticalSection(&m_csMem);
}
// 打开内存⽂件,dwMemBlockSize为缓存块⼤⼩,⼤于该值时进⾏⼀次IO操作
BOOL CFileMem::Open(const char* pszPathFile, DWORD dwMemBlockSize)
{
if(!pszPathFile)
return FALSE;
// 关闭之前已打开内存块
Close();
// 保存⽇志⽂件全路径名
strcpy(m_szPathFile, pszPathFile);
if(dwMemBlockSize <= 0)
return FALSE;
m_pMemBlock = (char*)malloc(dwMemBlockSize);
if(NULL == m_pMemBlock)
return FALSE;
memset(m_pMemBlock, 0, dwMemBlockSize);
m_dwMemBlockSize = dwMemBlockSize;
m_dwPos = 0;
return TRUE;
}
/*
写数据到内存⽂件,dwFileSize将返回当前磁盘⽂件的⼤⼩,
该值可⽤于外部判断⽇志⽂件是否过⼤,⽐如当dwFileSize⼤于多少M时,可重命名⽂件
从⽽避免单个⽇志⽂件过⼤
*/
BOOL CFileMem::Write(const char* pszLog, DWORD& dwFileSize)
{
return Write((const unsigned char*)pszLog, strlen(pszLog), dwFileSize);
}
BOOL CFileMem::Write(const unsigned char* pData, DWORD dwDataSize, DWORD& dwFileSize) {
dwFileSize = 0;
if(NULL == pData || 0 == dwDataSize)
return FALSE;
// 如果内部没有开辟缓冲区,则直接写⽂件
if(NULL == m_pMemBlock || 0 == m_dwMemBlockSize)
{
FILE* pFile = ::fopen(m_szPathFile, "ab+");
if(NULL == pFile)
return FALSE;
::fwrite(pData, 1, dwDataSize, pFile);
// 获取磁盘⽂件⼤⼩
::fseek(pFile, 0L, SEEK_END);
dwFileSize = ::ftell(pFile);
::fclose(pFile);
return TRUE;
}
::EnterCriticalSection(&m_csMem);
// 如果内存块已满,则写⼊磁盘⽂件
DWORD dwTotalSize = m_dwPos + dwDataSize;
if(dwTotalSize >= m_dwMemBlockSize)
{
FILE* pFile = ::fopen(m_szPathFile, "ab+");
if(NULL == pFile)
{
::LeaveCriticalSection(&m_csMem);
return FALSE;
}
// 将当前内存中数据写⼊⽂件
::fwrite(m_pMemBlock, 1, m_dwPos, pFile);
:
:fwrite(pData, 1, dwDataSize, pFile);
// 获取磁盘⽂件⼤⼩
::fseek(pFile, 0L, SEEK_END);
dwFileSize = ::ftell(pFile);
::fclose(pFile);
// 清空内存块
memset(m_pMemBlock, 0, m_dwMemBlockSize);
m_dwPos = 0;
}
else
{
/
/ 如果内存未满,将当前数据写⼊内存
memcpy(m_pMemBlock+m_dwPos, pData, dwDataSize);
m_dwPos += dwDataSize;
}
::LeaveCriticalSection(&m_csMem);
return TRUE;
}
// 将缓冲区的内容写⼊磁盘
BOOL CFileMem::Flush(DWORD& dwFileSize)
{
// 参数
dwFileSize = 0;
if(NULL == m_pMemBlock || 0 == m_dwMemBlockSize || 0 == m_dwPos)  return TRUE;
::EnterCriticalSection(&m_csMem);
FILE* pFile = ::fopen(m_szPathFile, "ab+");
if(NULL == pFile)
{
::LeaveCriticalSection(&m_csMem);
return FALSE;
}
// 内存数据写⼊磁盘⽂件
::fwrite(m_pMemBlock, 1, m_dwPos, pFile);
/
/ 获取磁盘⽂件⼤⼩
::fseek(pFile, 0L, SEEK_END);
dwFileSize = ::ftell(pFile);
::fclose(pFile);
// 清空内存块
memset(m_pMemBlock, 0, m_dwMemBlockSize);
m_dwPos = 0;
::LeaveCriticalSection(&m_csMem);
return TRUE;
}
void CFileMem::Close()
{
// 将内存中⽂件写⼊磁盘
DWORD dwFileSize = 0;
Flush(dwFileSize);
// 释放内存块
free(m_pMemBlock);
m_pMemBlock = NULL;
m_dwMemBlockSize = 0;
m_dwPos = 0;
memset(m_szPathFile, 0, 256);
}
/
/ 重命名⽂件
void CFileMem::Rename(const char* pszOldPathFile, const char* pszNewPathFile) {
::EnterCriticalSection(&m_csMem);
::rename(pszOldPathFile, pszNewPathFile);
::LeaveCriticalSection(&m_csMem);
}
测试函数:
#include <assert.h>
#include <stdio.h>
#include <time.h> // for Clock
// 传统的⽇志输出函数
void Logout(const char* pszLogFile, const char* pszLog)
{
// 参数检查
assert(pszLogFile);
assert(pszLog);
if(NULL == pszLogFile || NULL == pszLog)
return;
// 打开⽇志⽂件
FILE* pFile = fopen(pszLogFile, "ab+");
if(pFile)
{
/
/ 写⽇志
fwrite((void*)pszLog, 1, strlen(pszLog), pFile);
fclose(pFile);
}
}
static void TestFileMem()
{
const char* pszLogFile1 = "c:/test1.log";
const char* pszLogInfo = "Hello world\r\n";
int iCount  = 10000;
// 循环写⽇志1万次
/
/ 第⼀种⽅式:传统写⽇志⽅式
clock_t cStart = clock();
for(int i = 0; i < iCount; i++)
Logout(pszLogFile1, pszLogInfo);
clock_t cEnd = clock();
long lElapsed = cEnd - cStart;
printf("Logout Elpased time %d ms\r\n", lElapsed);
// 第⼆种⽅式:使⽤CFileMem写⽇志
const char* pszLogFile2 = "c:/test2.log";
CFileMem fm;
DWORD dwFileSize = 0;
fm.Open(pszLogFile2);
cStart = clock();
for(i = 0; i < iCount; i++)
fm.Write(pszLogInfo, dwFileSize);
fm.Close();
cEnd = clock();
lElapsed = cEnd - cStart;
printf("CFileMem Elpased time %d ms\r\n", lElapsed);
}
输出结果:
通过测试⽐较,第⼆种⽅式速度更加快速,⽽且是线程安全的。
我写的“”,就使⽤该类进⾏⽇志打印,效果不错。
感谢您的阅读!

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。