在上一篇文章别在C++代码里乱打日志了,这才是正确的打日志姿势!中,Jungle设计实现了C++日志系统,并将其用于之前已有的小程序中,测试结果也是OK的。那是否就说明这个Log系统没问题呢?
Too young too simple
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里已经完全乱了!)所以根本问题是,多个线程在同一个时刻访问了同一个资源!所以针对多线程环境,我们需要做到共享资源的互斥!
线程安全的日志系统
在单例模式的设计实现里已经提到了线程安全,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不同)。
2
注意事项
尽管上述已经基本实现了日志系统,但仍有很大的改进空间,在调试代码和查阅资料的过程中,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
最后,推荐两篇不错的关于日志系统的文章: