C++设计实现日志系统

news2024/10/7 15:26:17

转载:C++设计实现日志系统 - 知乎 (zhihu.com) 

日志系统几乎是每一个实际的软件项目从开发、测试到交付,再到后期的维护过程中极为重要的 查看软件代码运行流程还原错误现场记录运行错误位置及上下文等的重要依据。一个高性能的日志系统,能够准确记录重要的变量信息,同时又没有冗余的打印导致日志文件记录无效的数据。本文Jungle将用C++设计实现一个日志系统。

1.为什么需要日志

为什么需要日志?其实在引言中已经提到了,实际的软件项目的几乎每个过程,都离不开日志。初学代码时,Jungle的第一行代码是实现打印“hello world”,打印到控制台。在后来的学习中,Jungle又学会了设断点调试代码,在适当的地方通过断点来观察变量的值。但在实际的软件项目中,试想一下,通过输出到控制台或者通过设断点来调试代码,可能吗?

  • 客户现场,会让你现场打印到控制台上调试吗?
  • 报了error的软件项目,你能够明确知道软件crash的位置吗?
  • 你能保证设断点可以还原error时候的现场吗?
  • 概率性的error事件,设断点还奏效吗?
  • 如果是时效性的代码(比如USB连接) ,设断点调试还合理吗?
  • ……

日志,可以记录每一时刻软件的运行情况,记录error或者crash时的信息(时间、关键变量的值、出错位置、线程等);另一方面,对于概率性error事件,可以在重复测试时通过日志来查询错误复现时候的情况。简言之,日志是跟踪和回忆某个时刻或者时间段内的程序行为进而定位问题的一种重要手段

2.日志系统设计

软件运行过程中,需要记录的有什么呢?前述已经提到,关键变量的值、运行的位置(哪个文件、哪个函数、哪一行)、时间线程号进程号。本文Jungle采用C++设计了LOG类,介绍LOG类的设计之前,需要提及的是log的级别和log位置。

2.1.1.log级别

Log级别是什么意思呢?在开发阶段,Jungle可能想尽可能详细地跟踪代码运行过程,所以可以打印尽可能多的信息到日志文件中;测试过程中,测试部可能不需要这么详细的信息,所以这时候有的信息可能不必输出到Log文件;产品交付客户使用时,为了软件运行更快、客户体验更好,这时候就只需打印关键信息到日志文件了,因为过多的写文件会耗费大量时间,影响软件运行速度。所以Jungle为LOG类定义了如下级别:

enum LOGLEVEL
{
	LOG_LEVEL_NONE,
	LOG_LEVEL_ERROR,     // error
	LOG_LEVEL_WARNING,   // warning
	LOG_LEVEL_DEBUG,     // debug
	LOG_LEVEL_INFO,      // info	
};

在软件设计中,可以通过某些方法或者预留一些开关来设置Log级别,方便在开发、调试、测试和客户现场灵活地调整日志级别,以获取到有用的日志信息。

2.1.2.log输出位置

Log文件可以输出到控制台(其实也是不错的方法),也可以输出到指定路径下的某个文件里,也可能有别的需求。比如,开发或调试时,简单的信息直接就打印到软件某个界面上;测试或者交付客户时,最好将日志保存到文件里,这样可以保存尽可能多的信息。因此,Jungle进行了如下设计:

enum LOGTARGET
{
	LOG_TARGET_NONE      = 0x00,
	LOG_TARGET_CONSOLE   = 0x01,
	LOG_TARGET_FILE      = 0x10
};

2.1.3.log的作用域

一个软件系统,要在哪儿输出日志呢?Everywhere!只要是你想打印日志的地方,任何一个函数、任何一个文件,都应该而且必须可以打印。也就是说这个log类的对象(不妨叫做日志记录器),日志记录器必须是全局的

光是全局的就够了吗?你这个文件里有一个全局的日志记录器,输出日志到file.log文件里;另一个文件里也有一个日志记录器,也输出到file.log文件里……多个日志记录器同时往一个文件里写日志,这显然不合理。所以还必须保证日志记录器全局且唯一

怎么保证日志记录器唯一呢?即Log类在具体的软件系统中有且仅有一个实例化对象。答案是采用单例模式!(设计模式(九)——单例模式)

2.2.日志类的设计

综上所述,Jungle设计的日志类LOG如下:

class LOG
{
public:
 
	// 初始化
	void init(LOGLEVEL loglevel, LOGTARGET logtarget);
 
	// 
	void uninit();
 
	// file
	int createFile();
 
	static LOG* getInstance();
 
	// Log级别
	LOGLEVEL getLogLevel();
	void setLogLevel(LOGLEVEL loglevel);
 
	// Log输出位置
	LOGTARGET getLogTarget();
	void setLogTarget(LOGTARGET logtarget);
 
	// 打log
	static int writeLog(
		LOGLEVEL loglevel,         // Log级别
		unsigned char* fileName,   // 函数所在文件名
		unsigned char* function,   // 函数名
		int lineNumber,            // 行号
		char* format,              // 格式化
		...);                      // 变量
 
	// 输出log
	static void outputToTarget();
 
private:
	LOG();
	~LOG();
	static LOG* Log;
 
	// 互斥锁
	static mutex log_mutex;
 
	// 存储log的buffer
	static string logBuffer;
 
	// Log级别
	LOGLEVEL logLevel;
 
	// Log输出位置
	LOGTARGET logTarget;
 
	// Handle
	static HANDLE mFileHandle;
};

其中,互斥锁log_mutex是用于在多线程环境下保证只创建一个LOG类的实例 (设计模式(九)——单例模式);mFileHandle是log文件的句柄。

2.3.日志类的实现

2.3.1.初始化

LOG*             LOG::Log         = NULL;
string           LOG::logBuffer   = "";
HANDLE           LOG::mFileHandle = INVALID_HANDLE_VALUE;
mutex            LOG::log_mutex;
 
LOG::LOG()
{
	// 初始化
	init(LOG_LEVEL_NONE, LOG_TARGET_FILE);
} 
 
void LOG::init(LOGLEVEL loglevel, LOGTARGET logtarget)
{
	setLogLevel(loglevel);
	setLogTarget(logtarget);
	createFile();
}
 
void LOG::uninit()
{
	if (INVALID_HANDLE_VALUE != mFileHandle)
	{
		CloseHandle(mFileHandle);
	}
}
 
LOG* LOG::getInstance()
{
	if (NULL == Log)
	{
		log_mutex.lock();
		if (NULL == Log)
		{
			Log = new LOG();
		}
		log_mutex.unlock();
	}
	return Log;
}
 
LOGLEVEL LOG::getLogLevel()
{
	return this->logLevel;
}
 
void LOG::setLogLevel(LOGLEVEL iLogLevel)
{
	this->logLevel = iLogLevel;
}
 
LOGTARGET LOG::getLogTarget()
{
	return this->logTarget;
}
 
void LOG::setLogTarget(LOGTARGET iLogTarget)
{
	this->logTarget = iLogTarget;
}

初始化工作设置了日志的级别和输出位置(代码中提供了日志级别和输出位置的setter、getter方法)。函数createFile()是创建日志文件位置,并获取日志文件的句柄mFileHandle。代码如下:

int LOG::createFile()
{
	TCHAR fileDirectory[256];
	GetCurrentDirectory(256, fileDirectory);
 
	// 创建log文件的路径
	TCHAR logFileDirectory[256];
	_stprintf_s(logFileDirectory, _T("%s\\Test\\"), fileDirectory);// 使用_stprintf_s需要包含头文件<TCHAR.H>
 
	// 文件夹不存在则创建文件夹
	if (_taccess(logFileDirectory, 0) == -1)
	{
		_tmkdir(logFileDirectory);
	}
 
	TCHAR cTmpPath[MAX_PATH] = { 0 };
	TCHAR* lpPos = NULL;
	TCHAR cTmp = _T('\0');
 
	WCHAR pszLogFileName[256];
	// wcscat:连接字符串
	wcscat(logFileDirectory, _T("test.log"));
	_stprintf_s(pszLogFileName, _T("%s"), logFileDirectory);
	mFileHandle = CreateFile(
		pszLogFileName,
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ,
		NULL,
		OPEN_ALWAYS,
		FILE_ATTRIBUTE_NORMAL,
		NULL);
	if (INVALID_HANDLE_VALUE == mFileHandle)
	{
		return -1;
	}
	return 0;
}

其中,需要介绍的是下述函数:

  • GetCurrentDirectory:在一个缓冲区中装载当前目录
  • _stprintf_s:将若干个参数按照format格式存到buffer中
  • _taccess:判断文件是否存在,返回值0表示该文件存在,返回-1表示文件不存在或者该模式下没有访问权限
  • _tmkdir:创建一个目录

2.3.2.写日志

以下是writeLog()方法的实现:

int LOG::writeLog(
	LOGLEVEL loglevel,         // Log级别
	unsigned char* fileName,   // 函数所在文件名
	unsigned char* function,   // 函数名
	int lineNumber,            // 行号
	char* format,              // 格式化
	...)
{
	int ret = 0;
 
	// 获取日期和时间
	char timeBuffer[100];
	ret = getSystemTime(timeBuffer);
	logBuffer += string(timeBuffer);
 
	// LOG级别
	char* logLevel;
	if (loglevel == LOG_LEVEL_DEBUG){
		logLevel = "DEBUG";
	}
	else if (loglevel == LOG_LEVEL_INFO){
		logLevel = "INFO";
	}
	else if (loglevel == LOG_LEVEL_WARNING){
		logLevel = "WARNING";
	}
	else if (loglevel == LOG_LEVEL_ERROR){
		logLevel = "ERROR";
	}
 
	// [进程号][线程号][Log级别][文件名][函数名:行号]
	char locInfo[100];
	char* format2 = "[PID:%4d][TID:%4d][%s][%-s][%s:%4d]";
	ret = printfToBuffer(locInfo, 100, format2,
		GetCurrentProcessId(),
		GetCurrentThreadId(),
		logLevel,
		fileName,
		function,
		lineNumber);
	logBuffer += string(locInfo);	
 
	// 日志正文
	char logInfo2[256];
	va_list ap;
	va_start(ap, format);
	ret = vsnprintf(logInfo2, 256, format, ap);
	va_end(ap);
 
	logBuffer += string(logInfo2);
	logBuffer += string("\n");
 
	outputToTarget();
 
	return 0;
}

2.3.3.输出日志

void LOG::outputToTarget()
{
	if (LOG::getInstance()->getLogTarget() & LOG_TARGET_FILE)
	{
		SetFilePointer(mFileHandle, 0, NULL, FILE_END);
		DWORD dwBytesWritten = 0;
		WriteFile(mFileHandle, logBuffer.c_str(), logBuffer.length(), &dwBytesWritten, NULL);
		FlushFileBuffers(mFileHandle);
	}
	if (LOG::getInstance()->getLogTarget() & LOG_TARGET_CONSOLE)
	{
		printf("%s", logBuffer.c_str());
	}
 
	// 清除buffer
	logBuffer.clear();
}
  • SetFilePointer:将文件指针移动到文件指定的位置
  • FlushFileBuffers:把写文件缓冲区的数据强制写入磁盘

为了使用方便,可以定义一些宏来简化函数的使用,本文不再赘述。

3.测试

Jungle将上述设计实现的日志系统应用到了之前写的一些小程序里,比如在之前的“欲戴王冠,必承其重”——深度解析职责链模式的代码。如何添加呢?就是将两个文件(头文件和源文件)加入工程,包含头文件,再在需要打log的地方加上Jungle在日志类里定义的宏即可。下列是示例log:

因为程序比较简单,代码量很小,所以只有一个线程(log中TID都是一样的)。但上述测试结果验证了Jungle设计的日志系统是可行的。

4.多线程环境

4.1.多线程环境测试

接下来Jungle设计一个简单的多线程环境,测试一下上述日志系统,测试代码如下:

#define THREAD_NUM 5
// 全局资源变量
int g_num = 0;
 
unsigned int __stdcall func(void *pPM)
{
	LOG_INFO("enter");
	Sleep(50);
	g_num++;
	LOG_INFO("g_num = %d", g_num);
	LOG_INFO("exit");
	return 0;
}
 
int main()
{
	LOG *logger = LOG::getInstance();
	HANDLE  handle[THREAD_NUM];
 
	//线程编号
	int threadNum = 0;
	while (threadNum < THREAD_NUM)
	{
		handle[threadNum] = (HANDLE)_beginthreadex(NULL, 0, func, NULL, 0, NULL);
		//等子线程接收到参数时主线程可能改变了这个i的值
		threadNum++;
	}
	//保证子线程已全部运行结束
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
	return 0;
}

上述代码中,Jungle一共开启了5个线程,理论上打印的日志文件里,TID应该出现5个不同的数值。每个线程里打印全局变量(即全局共享资源)的值。下面是输出的日志,一共运行了两次(第5、6行隔开):

问题来啦!

首先,在第一次运行输出的日志里,出现了乱码!(第1行和第4行),而且看起来该输出log的地方没有完全输出(真的吗?)

其次,在第二次运行输出的日志里,一行log里好像打印了两次日志(第8行)!

问题出在哪里呢?

为什么会出现乱码?仔细看第8行log,其实打印的都是同一个时刻、同一个位置,都是在调用writeLog函数(宏LOG_INFO即是调用writeLog函数)时出现的问题,也就是说在这个时刻,两个线程都跑到函数writeLog里写log,导致logBuffer缓冲区里存放了两次信息。只不过第8行运气较好,每次的编码都保存完整。而第1行和第4行就没这么走运了!(logBuffer里已经完全乱了!)所以根本问题是,多个线程在同一个时刻访问了同一个资源!所以针对多线程环境,我们需要做到共享资源的互斥

4.2.线程安全的日志系统

在单例模式的设计实现里已经提到了线程安全,Jungle用互斥锁达到了互斥的目的。本文也可以使用互斥锁(并且在日志对象实例的单例模式中已经使用),但在这里Jungle想用另一种方法:临界区。

在Log类成员里声明一个CRITICAL_SECTION对象criticalSection,初始化时:

InitializeCriticalSection(&criticalSection);

当然,最好在释放资源时加上下述代码:

DeleteCriticalSection(&criticalSection);

而在进入writeLog时和离开writeLog时加上下述代码:

int LOG::writeLog(...)
{
	int ret = 0;
 
	EnterCriticalSection(&criticalSection);
    
        // do something
 
	LeaveCriticalSection(&criticalSection);
 
	return 0;
}

需要提及的是,最好是在LeaveCriticalSection之后再DeleteCriticalSection。

接下来再在多线程环境里测试,Jungle测试了几次,但为了缩短篇幅,只展示一次的结果:

可以看到,日志完整记录了每个线程的运行过程(线程号TID不同)。

5.注意事项

尽管上述已经基本实现了日志系统,但仍有很大的改进空间,在调试代码和查阅资料的过程中,Jungle发现需要注意以下几个问题:

(1)字符编码问题:宽字符、ANSI编码等多种不同编码的兼容;

(2)Visio Studio版本的差异:Jungle本想将日志系统应用到之前设计的一个机器人仿真控制器里,但遗憾的是编译不通过,因为那个代码是用Visio Studio 2008写的,而Mutex是C++2011标准的内容,需要用支持该新标准的编译器,比如VS2012及以上版本。(当然了,可以用临界区等其他方法实现互斥,这里Jungle只是提出这个需要注意的问题);

(3)关于宏_CRT_SECURE_NO_WARNINGS:是的,需要在预处理器里加上这个宏或者代码里显示声明这个宏,否则编译不通过,如下图。原因是代码中使用的wcscat等函数不安全,可能会造成内存泄露等。解决方法除了前述提到的声明宏以外,还可以使用更安全的函数

源码地址:

日志系统几乎是每一个实际的软件项目从开发、测试到交付,再到后期的维护过程中极为重要的 查看软件代码运行流程还原错误现场记录运行错误位置及上下文等的重要依据。一个高性能的日志系统,能够准确记录重要的变量信息,同时又没有冗余的打印导致日志文件记录无效的数据。本文Jungle将用C++设计实现一个日志系统。

1.为什么需要日志

为什么需要日志?其实在引言中已经提到了,实际的软件项目的几乎每个过程,都离不开日志。初学代码时,Jungle的第一行代码是实现打印“hello world”,打印到控制台。在后来的学习中,Jungle又学会了设断点调试代码,在适当的地方通过断点来观察变量的值。但在实际的软件项目中,试想一下,通过输出到控制台或者通过设断点来调试代码,可能吗?

  • 客户现场,会让你现场打印到控制台上调试吗?
  • 报了error的软件项目,你能够明确知道软件crash的位置吗?
  • 你能保证设断点可以还原error时候的现场吗?
  • 概率性的error事件,设断点还奏效吗?
  • 如果是时效性的代码(比如USB连接) ,设断点调试还合理吗?
  • ……

日志,可以记录每一时刻软件的运行情况,记录error或者crash时的信息(时间、关键变量的值、出错位置、线程等);另一方面,对于概率性error事件,可以在重复测试时通过日志来查询错误复现时候的情况。简言之,日志是跟踪和回忆某个时刻或者时间段内的程序行为进而定位问题的一种重要手段

2.日志系统设计

软件运行过程中,需要记录的有什么呢?前述已经提到,关键变量的值、运行的位置(哪个文件、哪个函数、哪一行)、时间线程号进程号。本文Jungle采用C++设计了LOG类,介绍LOG类的设计之前,需要提及的是log的级别和log位置。

2.1.1.log级别

Log级别是什么意思呢?在开发阶段,Jungle可能想尽可能详细地跟踪代码运行过程,所以可以打印尽可能多的信息到日志文件中;测试过程中,测试部可能不需要这么详细的信息,所以这时候有的信息可能不必输出到Log文件;产品交付客户使用时,为了软件运行更快、客户体验更好,这时候就只需打印关键信息到日志文件了,因为过多的写文件会耗费大量时间,影响软件运行速度。所以Jungle为LOG类定义了如下级别:

enum LOGLEVEL
{
	LOG_LEVEL_NONE,
	LOG_LEVEL_ERROR,     // error
	LOG_LEVEL_WARNING,   // warning
	LOG_LEVEL_DEBUG,     // debug
	LOG_LEVEL_INFO,      // info	
};

在软件设计中,可以通过某些方法或者预留一些开关来设置Log级别,方便在开发、调试、测试和客户现场灵活地调整日志级别,以获取到有用的日志信息。

2.1.2.log输出位置

Log文件可以输出到控制台(其实也是不错的方法),也可以输出到指定路径下的某个文件里,也可能有别的需求。比如,开发或调试时,简单的信息直接就打印到软件某个界面上;测试或者交付客户时,最好将日志保存到文件里,这样可以保存尽可能多的信息。因此,Jungle进行了如下设计:

enum LOGTARGET
{
	LOG_TARGET_NONE      = 0x00,
	LOG_TARGET_CONSOLE   = 0x01,
	LOG_TARGET_FILE      = 0x10
};

2.1.3.log的作用域

一个软件系统,要在哪儿输出日志呢?Everywhere!只要是你想打印日志的地方,任何一个函数、任何一个文件,都应该而且必须可以打印。也就是说这个log类的对象(不妨叫做日志记录器),日志记录器必须是全局的

光是全局的就够了吗?你这个文件里有一个全局的日志记录器,输出日志到file.log文件里;另一个文件里也有一个日志记录器,也输出到file.log文件里……多个日志记录器同时往一个文件里写日志,这显然不合理。所以还必须保证日志记录器全局且唯一

怎么保证日志记录器唯一呢?即Log类在具体的软件系统中有且仅有一个实例化对象。答案是采用单例模式!(设计模式(九)——单例模式)

2.2.日志类的设计

综上所述,Jungle设计的日志类LOG如下:

class LOG
{
public:
 
	// 初始化
	void init(LOGLEVEL loglevel, LOGTARGET logtarget);
 
	// 
	void uninit();
 
	// file
	int createFile();
 
	static LOG* getInstance();
 
	// Log级别
	LOGLEVEL getLogLevel();
	void setLogLevel(LOGLEVEL loglevel);
 
	// Log输出位置
	LOGTARGET getLogTarget();
	void setLogTarget(LOGTARGET logtarget);
 
	// 打log
	static int writeLog(
		LOGLEVEL loglevel,         // Log级别
		unsigned char* fileName,   // 函数所在文件名
		unsigned char* function,   // 函数名
		int lineNumber,            // 行号
		char* format,              // 格式化
		...);                      // 变量
 
	// 输出log
	static void outputToTarget();
 
private:
	LOG();
	~LOG();
	static LOG* Log;
 
	// 互斥锁
	static mutex log_mutex;
 
	// 存储log的buffer
	static string logBuffer;
 
	// Log级别
	LOGLEVEL logLevel;
 
	// Log输出位置
	LOGTARGET logTarget;
 
	// Handle
	static HANDLE mFileHandle;
};

其中,互斥锁log_mutex是用于在多线程环境下保证只创建一个LOG类的实例 (设计模式(九)——单例模式);mFileHandle是log文件的句柄。

2.3.日志类的实现

2.3.1.初始化

LOG*             LOG::Log         = NULL;
string           LOG::logBuffer   = "";
HANDLE           LOG::mFileHandle = INVALID_HANDLE_VALUE;
mutex            LOG::log_mutex;
 
LOG::LOG()
{
	// 初始化
	init(LOG_LEVEL_NONE, LOG_TARGET_FILE);
} 
 
void LOG::init(LOGLEVEL loglevel, LOGTARGET logtarget)
{
	setLogLevel(loglevel);
	setLogTarget(logtarget);
	createFile();
}
 
void LOG::uninit()
{
	if (INVALID_HANDLE_VALUE != mFileHandle)
	{
		CloseHandle(mFileHandle);
	}
}
 
LOG* LOG::getInstance()
{
	if (NULL == Log)
	{
		log_mutex.lock();
		if (NULL == Log)
		{
			Log = new LOG();
		}
		log_mutex.unlock();
	}
	return Log;
}
 
LOGLEVEL LOG::getLogLevel()
{
	return this->logLevel;
}
 
void LOG::setLogLevel(LOGLEVEL iLogLevel)
{
	this->logLevel = iLogLevel;
}
 
LOGTARGET LOG::getLogTarget()
{
	return this->logTarget;
}
 
void LOG::setLogTarget(LOGTARGET iLogTarget)
{
	this->logTarget = iLogTarget;
}

初始化工作设置了日志的级别和输出位置(代码中提供了日志级别和输出位置的setter、getter方法)。函数createFile()是创建日志文件位置,并获取日志文件的句柄mFileHandle。代码如下:

int LOG::createFile()
{
	TCHAR fileDirectory[256];
	GetCurrentDirectory(256, fileDirectory);
 
	// 创建log文件的路径
	TCHAR logFileDirectory[256];
	_stprintf_s(logFileDirectory, _T("%s\\Test\\"), fileDirectory);// 使用_stprintf_s需要包含头文件<TCHAR.H>
 
	// 文件夹不存在则创建文件夹
	if (_taccess(logFileDirectory, 0) == -1)
	{
		_tmkdir(logFileDirectory);
	}
 
	TCHAR cTmpPath[MAX_PATH] = { 0 };
	TCHAR* lpPos = NULL;
	TCHAR cTmp = _T('\0');
 
	WCHAR pszLogFileName[256];
	// wcscat:连接字符串
	wcscat(logFileDirectory, _T("test.log"));
	_stprintf_s(pszLogFileName, _T("%s"), logFileDirectory);
	mFileHandle = CreateFile(
		pszLogFileName,
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ,
		NULL,
		OPEN_ALWAYS,
		FILE_ATTRIBUTE_NORMAL,
		NULL);
	if (INVALID_HANDLE_VALUE == mFileHandle)
	{
		return -1;
	}
	return 0;
}

其中,需要介绍的是下述函数:

  • GetCurrentDirectory:在一个缓冲区中装载当前目录
  • _stprintf_s:将若干个参数按照format格式存到buffer中
  • _taccess:判断文件是否存在,返回值0表示该文件存在,返回-1表示文件不存在或者该模式下没有访问权限
  • _tmkdir:创建一个目录

2.3.2.写日志

以下是writeLog()方法的实现:

int LOG::writeLog(
	LOGLEVEL loglevel,         // Log级别
	unsigned char* fileName,   // 函数所在文件名
	unsigned char* function,   // 函数名
	int lineNumber,            // 行号
	char* format,              // 格式化
	...)
{
	int ret = 0;
 
	// 获取日期和时间
	char timeBuffer[100];
	ret = getSystemTime(timeBuffer);
	logBuffer += string(timeBuffer);
 
	// LOG级别
	char* logLevel;
	if (loglevel == LOG_LEVEL_DEBUG){
		logLevel = "DEBUG";
	}
	else if (loglevel == LOG_LEVEL_INFO){
		logLevel = "INFO";
	}
	else if (loglevel == LOG_LEVEL_WARNING){
		logLevel = "WARNING";
	}
	else if (loglevel == LOG_LEVEL_ERROR){
		logLevel = "ERROR";
	}
 
	// [进程号][线程号][Log级别][文件名][函数名:行号]
	char locInfo[100];
	char* format2 = "[PID:%4d][TID:%4d][%s][%-s][%s:%4d]";
	ret = printfToBuffer(locInfo, 100, format2,
		GetCurrentProcessId(),
		GetCurrentThreadId(),
		logLevel,
		fileName,
		function,
		lineNumber);
	logBuffer += string(locInfo);	
 
	// 日志正文
	char logInfo2[256];
	va_list ap;
	va_start(ap, format);
	ret = vsnprintf(logInfo2, 256, format, ap);
	va_end(ap);
 
	logBuffer += string(logInfo2);
	logBuffer += string("\n");
 
	outputToTarget();
 
	return 0;
}

2.3.3.输出日志

void LOG::outputToTarget()
{
	if (LOG::getInstance()->getLogTarget() & LOG_TARGET_FILE)
	{
		SetFilePointer(mFileHandle, 0, NULL, FILE_END);
		DWORD dwBytesWritten = 0;
		WriteFile(mFileHandle, logBuffer.c_str(), logBuffer.length(), &dwBytesWritten, NULL);
		FlushFileBuffers(mFileHandle);
	}
	if (LOG::getInstance()->getLogTarget() & LOG_TARGET_CONSOLE)
	{
		printf("%s", logBuffer.c_str());
	}
 
	// 清除buffer
	logBuffer.clear();
}
  • SetFilePointer:将文件指针移动到文件指定的位置
  • FlushFileBuffers:把写文件缓冲区的数据强制写入磁盘

为了使用方便,可以定义一些宏来简化函数的使用,本文不再赘述。

3.测试

Jungle将上述设计实现的日志系统应用到了之前写的一些小程序里,比如在之前的“欲戴王冠,必承其重”——深度解析职责链模式的代码。如何添加呢?就是将两个文件(头文件和源文件)加入工程,包含头文件,再在需要打log的地方加上Jungle在日志类里定义的宏即可。下列是示例log:

因为程序比较简单,代码量很小,所以只有一个线程(log中TID都是一样的)。但上述测试结果验证了Jungle设计的日志系统是可行的。

4.多线程环境

4.1.多线程环境测试

接下来Jungle设计一个简单的多线程环境,测试一下上述日志系统,测试代码如下:

#define THREAD_NUM 5
// 全局资源变量
int g_num = 0;
 
unsigned int __stdcall func(void *pPM)
{
	LOG_INFO("enter");
	Sleep(50);
	g_num++;
	LOG_INFO("g_num = %d", g_num);
	LOG_INFO("exit");
	return 0;
}
 
int main()
{
	LOG *logger = LOG::getInstance();
	HANDLE  handle[THREAD_NUM];
 
	//线程编号
	int threadNum = 0;
	while (threadNum < THREAD_NUM)
	{
		handle[threadNum] = (HANDLE)_beginthreadex(NULL, 0, func, NULL, 0, NULL);
		//等子线程接收到参数时主线程可能改变了这个i的值
		threadNum++;
	}
	//保证子线程已全部运行结束
	WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
	return 0;
}

上述代码中,Jungle一共开启了5个线程,理论上打印的日志文件里,TID应该出现5个不同的数值。每个线程里打印全局变量(即全局共享资源)的值。下面是输出的日志,一共运行了两次(第5、6行隔开):

问题来啦!

首先,在第一次运行输出的日志里,出现了乱码!(第1行和第4行),而且看起来该输出log的地方没有完全输出(真的吗?)

其次,在第二次运行输出的日志里,一行log里好像打印了两次日志(第8行)!

问题出在哪里呢?

为什么会出现乱码?仔细看第8行log,其实打印的都是同一个时刻、同一个位置,都是在调用writeLog函数(宏LOG_INFO即是调用writeLog函数)时出现的问题,也就是说在这个时刻,两个线程都跑到函数writeLog里写log,导致logBuffer缓冲区里存放了两次信息。只不过第8行运气较好,每次的编码都保存完整。而第1行和第4行就没这么走运了!(logBuffer里已经完全乱了!)所以根本问题是,多个线程在同一个时刻访问了同一个资源!所以针对多线程环境,我们需要做到共享资源的互斥

4.2.线程安全的日志系统

在单例模式的设计实现里已经提到了线程安全,Jungle用互斥锁达到了互斥的目的。本文也可以使用互斥锁(并且在日志对象实例的单例模式中已经使用),但在这里Jungle想用另一种方法:临界区。

在Log类成员里声明一个CRITICAL_SECTION对象criticalSection,初始化时:

InitializeCriticalSection(&criticalSection);

当然,最好在释放资源时加上下述代码:

DeleteCriticalSection(&criticalSection);

而在进入writeLog时和离开writeLog时加上下述代码:

int LOG::writeLog(...)
{
	int ret = 0;
 
	EnterCriticalSection(&criticalSection);
    
        // do something
 
	LeaveCriticalSection(&criticalSection);
 
	return 0;
}

需要提及的是,最好是在LeaveCriticalSection之后再DeleteCriticalSection。

接下来再在多线程环境里测试,Jungle测试了几次,但为了缩短篇幅,只展示一次的结果:

可以看到,日志完整记录了每个线程的运行过程(线程号TID不同)。

5.注意事项

尽管上述已经基本实现了日志系统,但仍有很大的改进空间,在调试代码和查阅资料的过程中,Jungle发现需要注意以下几个问题:

(1)字符编码问题:宽字符、ANSI编码等多种不同编码的兼容;

(2)Visio Studio版本的差异:Jungle本想将日志系统应用到之前设计的一个机器人仿真控制器里,但遗憾的是编译不通过,因为那个代码是用Visio Studio 2008写的,而Mutex是C++2011标准的内容,需要用支持该新标准的编译器,比如VS2012及以上版本。(当然了,可以用临界区等其他方法实现互斥,这里Jungle只是提出这个需要注意的问题);

(3)关于宏_CRT_SECURE_NO_WARNINGS:是的,需要在预处理器里加上这个宏或者代码里显示声明这个宏,否则编译不通过,如下图。原因是代码中使用的wcscat等函数不安全,可能会造成内存泄露等。解决方法除了前述提到的声明宏以外,还可以使用更安全的函数

源码地址:

https://github.com/FengJungle/Log​github.com/FengJungle/Log

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1208527.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Cloud学习(八)【RabbitMQ 服务异步通讯】

文章目录 初识 MQ同步通讯异步通讯MQ 常见框架 RabbitMQ 快速入门RabbitMQ 单机部署RabbitMQ概述常见消息模型 SpringAMQPSimpleQueue 模型WorkQueue 模型发布订阅模型发布订阅-Fanout Exchange发布订阅-DirectExchange发布订阅-TopicExchange消息转换器 初识 MQ 同步通讯 同步…

[Linux] DHCP网络

一、DHCP服务 1.1 DHCP的简介 DHCP&#xff08;Dynamic Host Configuration Protocol&#xff0c;动态主机配置协议&#xff09;通常被应用在大型的局域网络环境中&#xff0c;主要作用是集中地管理、分配IP地址&#xff0c;使网络环境中的主机动态的获得IP地址、Gateway地址…

Mistral 7B 比Llama 2更好的开源大模型 (二)

Mistral 7B 论文学习 Mistral 7B 论文链接 https://arxiv.org/abs/2310.06825 代码: https://github.com/mistralai/mistral-src 网站: https://mistral.ai/news/announcing-mistral-7b/ 论文摘要 Mistral 7B是一个70亿参数的语言模型,旨在获得卓越的性能和效率。Mistral 7…

【C++历险记】STL之set详解

个人主页&#xff1a;兜里有颗棉花糖&#x1f4aa; 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【C之路】&#x1f48c; 本专栏旨在记录C的学习路线&#xff0c;望对大家有所帮助&#x1f647;‍ 希望我们一起努力、成长&…

29.第三方登录

1►第三方登录 当今社会&#xff0c;微信登录、QQ登录、抖音登录等等三方登录已经层出不穷&#xff0c;学会三方登录势在必行。 微信登录要认证开发者&#xff0c;必须为企业&#xff0c;个人不行&#xff0c;而且还要交300块钱。 QQ登录也要申请、微博登录也要申请。 还好…

为什么重写equals方法必须重写hashcode方法

在Java中&#xff0c;重写equals()方法的同时也应该重写hashCode()方法&#xff0c;这是因为这两个方法在 Java 中是有关联的&#xff0c;而且它们一起影响着集合类的行为。 Java中的hashCode()方法用于返回对象的哈希码&#xff0c;而equals()方法用于比较两个对象是否相等。…

[文件读取]webgrind 文件读取 (CVE-2018-12909)

1.1漏洞描述 漏洞编号CVE-2018-12909漏洞类型文件读取漏洞等级⭐⭐⭐漏洞环境VULFOCUS攻击方式 1.2漏洞等级 高危 1.3影响版本 Webgrind 1.5版本 1.4漏洞复现 1.4.1.基础环境 1.4.2.前提 网站后台地址&#xff1a; 后台管理账密&#xff1a; 后台登录地址 1.5深度利用 …

【rl-agents代码学习】02——DQN算法

文章目录 Highway-env Intersectionrl-agents之DQN*Implemented variants*:*References*:Query agent for actions sequence探索策略神经网络实现小结1 Record the experienceReplaybuffercompute_bellman_residualstep_optimizerupdate_target_network小结2 exploration_polic…

spring 整合 JUnit

大家好&#xff0c;本篇博客我们通过spring来整合JUnitt单元测试框架。 在之前篇章的测试方法中&#xff0c;几乎都能看到以下的两行代码&#xff1a; ApplicationContext context new ClassPathXmlApplicationContext("xxx.xml"); Xxxx xxx context.getBean(Xxx…

ppt画思路图 流程图 医学药学生画图素材

关注微信&#xff0c;回复: 素材 &#xff0c;即可领取

EtherNET转Profibus网关使用 AB PLC的配置方法

兴达易控EtherNET转Profibus网关&#xff08;XD-EPPB20&#xff09;是一款功能强大的通讯设备&#xff0c;具备Profibus从站功能。它的主要作用是将EtherNET/IP设备无缝接入到PROFIBUS网络中。通过连接到Profibus总线&#xff0c;它可以作为从站使用&#xff0c;并且通过连接到…

作为8年老测试告诉你学会这样写性能测试方案,阿里p8都直呼内行

订单处理服务的性能测试方案V1.0 一、需求背景 在设计评审之后&#xff0c;开发在不知道服务性能瓶颈&#xff0c;需要测试协助定位服务的性能瓶颈&#xff0c;需要测试模拟一定时间之内设计并发用户同时向系统发出请求&#xff0c;检测出系统的响应能力&#xff0c;包括响应…

企业数字化建设之——老板关注的IT指标有哪些 ?

投资回报ROI | 商业价值 | 系统可用性 | 业务的参与程度 | 技术债务指数 降本&#xff0c;增效是IT部门工作的永恒话题 &#xff0c;降低成本 &#xff0c;增加效益 &#xff0c;降本增效的工作方向&#xff1a; 1 年初KPI目标、目标完成情况、关键证据、公司主线工作…

腾讯云2核4G和4核8G服务器配置5年租用价格表

腾讯云服务器网整理五年云服务器活动 txyfwq.com/go/txy 配置可选2核4G和4核8G&#xff0c;公网带宽可选1M、3M或5M&#xff0c;系统盘为50G高性能云硬盘&#xff0c;标准型S5实例CPU采用主频2.5GHz的Intel Xeon Cascade Lake或者Intel Xeon Cooper Lake处理器&#xff0c;睿频…

Java的XWPFTemplate word生成列表

Java的XWPFTemplate工具类导出word.docx的使用_xwpftemplate 语法_youmdt的博客-CSDN博客 如果是表格的列表参考上面这篇文章即可&#xff0c;比较复杂的列表遍历暂时还没找到方法&#xff0c;只能手动创建表格了 上面是模板&#xff0c;非常简单&#xff0c;以为我们是要自己创…

SELF-AUGMENTED MULTI-MODAL FEATURE EMBEDDING

two embeddings f o r g _{org} org​ and f a u g _{aug} aug​ are combined using a gating mechanism 作者未提供代码

修改Conda虚拟环境默认位置失败——解决方案

修改虚拟环境默认安装位置依然❌ 偶然遇到conda创建虚拟环境创建失败的问题&#xff0c;按照教程修改.condarc文件中 envs_dirs:- E:\miniconda3\envs依然无法更改虚拟环境默认安装位置。 解决方案 找到想更改的虚拟环境文件位置&#xff0c;检查确认是否有写入权限&#…

C++ builder 常见问题汇总

1、CB静态编译设置 2、CB10.3设置经典编译器&#xff08;用于解决10.3弹出代码提示慢&#xff09; 3、CBuilder生成Release版本 &#xff1a; project->Options->CCompiler->Build Configuration 选择 Release project->Options->CLinker中取消Use dynamic RTL…

upload-labs关卡5(点和空格绕过)通关思路

文章目录 前言一、回顾上一关知识点二、靶场第五关通关思路1.看源代码2.点和空格绕过3、验证上传 总结 前言 此文章只用于学习和反思巩固文件上传漏洞知识&#xff0c;禁止用于做非法攻击。注意靶场是可以练习的平台&#xff0c;不能随意去尚未授权的网站做渗透测试&#xff0…

MES系统如何改进生产管理?

伴随机械制造业行业竞争逐渐加剧&#xff0c;越来越多企业意识到MES系统的重要性&#xff0c;慢慢积极主动把握和实施MES系统。可是纵观绝大部分企业或者MES生产商&#xff0c;对MES的掌握依然存在比较大的分歧。 有一些人说MES系统是企业信息化构建的中枢神经&#xff0c;也有…