C++设计实现日志系统

转载: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.mfbz.cn/a/147126.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【ArcGIS Pro微课1000例】0032:创建具有指定高程Z值的矢量数据

本文讲解ArcGIS Pro中创建具有指定高程值的矢量数据的两种方法。 文章目录 一、独立创建1. 新建地图场景2. 新建shapefile3. 绘制多边形4. 添加高程字段5. 三维显示二、基于高程源创建1. 创建栅格范围2. 添加Z值字段3. 添加Z信息4. 要素更新Z值一、独立创建 1. 新建地图场景 …

Pytorch多GPU并行训练: DistributedDataParallel

1 模型并行化训练 1.1 为什么要并行训练 在训练大型数据集或者很大的模型时一块GPU很难放下&#xff0c;例如最初的AlexNet就是在两块GPU上计算的。并行计算一般采取两个策略&#xff1a;一个是模型并行&#xff0c;一个是数据并行。左图中是将模型的不同部分放在不同GPU上进…

【vue】AntDV组件库中a-upload实现文件上传:

文章目录 一、文档&#xff1a;二、使用(以Jeecg为例)&#xff1a;【1】template&#xff1a;【2】script&#xff1a; 三、效果图&#xff1a; 一、文档&#xff1a; Upload 上传–Ant Design Vue 二、使用(以Jeecg为例)&#xff1a; 【1】template&#xff1a; <a-uploa…

Springboot项目返回数据统一封装

Springboot项目返回数据统一封装,支持swagger。 正常swagger会根据数据库表的注释显示对应的参数释义等。但当我们使用统一接口返回map时&#xff0c;部分注释等信息会被掩盖消失。在此提供三个java类即可满足统一封装返回接口&#xff0c;也可显示对应的swagger释义等。 1.Er…

Vue 2学习(路由、history 和 hash 模式、)-day014

一、路由简介 路由&#xff08;route&#xff09;就是一组 key-value 的对应关系多个路由&#xff0c;需要经过路由器&#xff08;router&#xff09;的管理 在 Vue 中也有路由&#xff0c;Vue 中的路由主要是通过 vue-rounter 这个插件库来实现&#xff0c;它的作用就是专门用…

php 插入排序算法实现

插入排序是一种简单直观的排序算法&#xff0c;它的基本思想是将一个数据序列分为有序区和无序区&#xff0c;每次从无序区选择一个元素插入到有序区的合适位置&#xff0c;直到整个序列有序为止 5, 3, 8, 2, 0, 1 HP中可以使用以下代码实现插入排序算法&#xff1a; functi…

【考研复习】二叉树的特殊存储|三叉链表存储二叉树、一维数组存储二叉树、线索二叉树

文章目录 三叉链表存储二叉树三叉链表的前序遍历&#xff08;不使用栈&#xff09;法一三叉链表的前序遍历&#xff08;不使用栈&#xff09;法二 一维数组存储二叉树一维数组存储二叉树的先序遍历 线索二叉树的建立真题演练 三叉链表存储二叉树 三叉链表结构体表示如下图所示…

2023-11-15 LeetCode每日一题(K 个元素的最大和)

2023-11-15每日一题 一、题目编号 2656. K 个元素的最大和二、题目链接 点击跳转到题目位置 三、题目描述 给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。你需要执行以下操作 恰好 k 次&#xff0c;最大化你的得分&#xff1a; 从 nums 中选择一个元素 m 。将选中…

三极管工作原理介绍(动画图解)

三极管BJT 各位社区的小伙伴们大家好&#xff0c;相信大家也像我一样&#xff0c;因为上学的时候学过&#xff0c;但是呢&#xff0c;出社会不怎么用&#xff0c;久而久之就忘了&#xff0c;今天来给大家巩固一下&#xff0c;我对三极管的通俗易懂的工作原理介绍&#xff01; 半…

计算机视觉+深度学习+机器学习+opencv+目标检测跟踪+一站式学习(代码+视频+PPT)

第1章&#xff1a;视觉项目资料介绍与学习指南 相关知识&#xff1a; 介绍计算机视觉、OpenCV库&#xff0c;以及课程的整体结构。学习概要&#xff1a; 了解课程的目标和学习路径&#xff0c;为后续章节做好准备。重要性&#xff1a; 提供学生对整个课程的整体认识&#xff0…

Python 如何实现组合(Composite)设计模式?什么是组合设计模式?

什么是组合&#xff08;Composite&#xff09;设计模式&#xff1f; 组合&#xff08;Composite&#xff09;设计模式是一种结构型设计模式&#xff0c;它允许客户端使用单一对象和组合对象&#xff08;对象的组合形成树形结构&#xff09;同样的方式处理。这样&#xff0c;客…

c++之xml的创建,增删改查

c之xml的创建&#xff0c;增删改查 1.创建写入2.添加3.删除4.修改&#xff1a; 1.创建写入 #include <stdio.h> #include <typeinfo> #include "F:/EDGE/tinyxml/tinyxml.h" #include <iostream> #include <string> #include <Winsock2.…

王道数据结构课后代码题p40 6.有一个带头结点的单链表L,设计一个算法使其元素递增有序 (c语言代码实现)

这一题其实用到了直接插入排序的思想 视频讲解在这里哦&#xff1a;&#x1f447; p40 第6题 王道数据结构课后代码题 c语言代码实现_哔哩哔哩_bilibili 本题代码为 void paixu(linklist* L)//对单链表内的元素排序 {lnode* p (*L)->next;lnode* pre *L;lnode* r p-&…

Skybox天空盒子的更换教程_unity基础开发教程

Skybox天空盒子的更换 Skybox的下载与导入更换SkyboxSkybox属性自定义 Skybox的下载与导入 打开资源商店 搜索FREE Skybox 这里是我使用的是这一款资源&#xff0c;点击添加至我的资源 打开包管理器Package Manager Packages选择My Assets 搜索Sky 选择刚刚添加的天空盒子 点…

3D Gaussian Splatting文件的压缩【3D高斯泼溅】

在上一篇文章中&#xff0c;我开始研究高斯泼溅&#xff08;3DGS&#xff1a;3D Gaussian Splatting&#xff09;。 它的问题之一是数据集并不小。 渲染图看起来不错。 但“自行车”、“卡车”、“花园”数据集分别是一个 1.42GB、0.59GB、1.35GB 的 PLY 文件。 它们几乎按原样…

西门子精彩触摸屏SMART LINE V4 面板使用U盘下载项目程序的具体方法示例

西门子精彩触摸屏SMART LINE V4 面板使用U盘下载项目程序的具体方法示例 WinCC flexible SMART V4 SP1 软件针对SMART LINE V4 面板新增了使用U盘下载项目功能。 注意:“使用U盘下载项目”功能仅支持触摸屏OS版本为V4.0.1.0 及以上的设备。 使用U盘下载项目的步骤可参考以下内…

php+vue3实现点选验证码

buildadmin 中的点选验证码实现 验证码类 <?phpnamespace ba;use Throwable; use think\facade\Db; use think\facade\Lang; use think\facade\Config;/*** 点选文字验证码类*/ class ClickCaptcha {/*** 验证码过期时间(s)* var int*/private int $expire 600;/*** 可以…

【洛谷算法题】P5711-闰年判断【入门2分支结构】

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【洛谷算法题】 文章目录 【洛谷算法题】P5711-闰年判断【入门2分支结构】&#x1f30f;题目描述&#x1f30f;输入格式&a…

【LeetCode刷题笔记】二叉树(二)

257. 二叉树的所有路径 解题思路: 1. DFS 前序遍历 ,每次递归将 当前节点的拼接结果 传递到 下一层 中,如果当前节点是 叶子节点 ,就将 当前拼接结果 收集答案并返回。 注意:路径path结果可以使用 String 来拼接,这样可以避免回溯处理。

Scala---方法与函数

一、Scala方法的定义 有参方法&无参方法 def fun (a: Int , b: Int) : Unit {println(ab) } fun(1,1)def fun1 (a: Int , b: Int) ab println(fun1(1,2)) 注意点&#xff1a; 方法定义语法 用def来定义可以定义传入的参数&#xff0c;要指定传入参数的类型方法可以写返…
最新文章