需求:实现一个类,要求这个类能实现日志的异步打印,打印信息包含时间,文件名,行号,进程号以及用户的打印信息。
这个需求里面的技术点。
1.异步的实现,肯定需要利用到线程?怎么保证线程同步?
2.文件名行号的获取?
3.怎么实现类似于printf的格式化输出的效果?
第一个问题:实现异步写日志
利用stl标准库里面的std::thread,结合std::mutex(共享锁)和std::condition_variable(条件变量)来实现。
另外这个类里面需要定义一个list类,list于vector类似,但是实现是基于双向链表的。在工作线程中,我们只需要将需要打印的string类加入到类Log的list链表内,同时notify_one()唤醒一次工作线程即可(该函数与notify_all()功能类似,都是用来唤醒线程),这里唤醒一次,处理一次队列里面的内容,然后线程休眠,阻塞在wait处。
多线程相关建议使用C++11的stl标准库。
对应的类是std::thread类,构造方法是std::thread(function,arg…);
相关的辅助类方法有
std::thread.join() //等待thread执行,再返回调用线程执行
std::thread.detatch() //线程分离
std::thread.joinable() //判断线程是否可以join
std::thread.get_id() //获取线程id
yield(); //当前线程放弃执行
在多线程调用时,需要用到锁。比如对类中的待打印队列进行操作时,如果不加锁,就很可能出现多个线程同时对类的打印队列进行操作。
最初始的mutex锁使用方法比较简单,如下
mutex m;
void func()
{
m.lock();
//do something
m.unlock();
}
以前的这种形式在很多时候容易出现忘记解锁的情况,导致出现死锁。
所以新的版本又出现了lock_guard,这个可以不用用户手动解锁,构造一个类,在出作用域的时候会直接调用析构函数帮用户解锁,避免出现死锁的情况。
mutex m;
void func()
{
std::lock_guard<mutex > mylock(m); //这里在构造的时候就已经上锁了
//dosomething
//函数结束时自动调用mylock的析构函数释放锁
}
lock_guard也可以通过传入两个参数来手动上锁,另外声明的lock_guard是不能手动加锁解锁的。
后面又引入了一个新的类unique_lock,他是lock_guard的升级版,用法更加丰富,可以通过构造参数来控制是否可以手动控制加解锁。像前两种锁都是直接先加锁操作,然后解锁,但是如果一直获取不到锁,程序就会一直阻塞。unique_lock提供了try_to_lock构造方法,传入该构造参数时会尝试获取一次锁(加锁),然后用户可以通过owns_lock方法判断是否获取到了锁。
void func()
{
unique_lock<mutex> g2(m,try_to_lock);//尝试加锁一次,但如果没有锁定成功,会立即返回,不会阻塞在那里,且不会再次尝试锁操作。
if(g2.owns_lock){//锁成功
//do something
}else{//锁失败则执行这段语句
//do something
}
}
另外还有一个比较重要的是condition_variable(条件变量),条件变量不是用来管理锁的,而是在获取锁之前对条件判断,看是否满足条件,不满足则不去尝试获取锁。调用wait函数时,需要保证wait传递的锁可以获取,并且条件被触发(即有地方调用了notify_all或者notify_one),通过下面的实验可以验证,单有notify是没办法让wait停止阻塞的。
void func1()
{
unique_lock<mutex> lock_guard(writelock);
my_variable.wait(lock_guard);
std::cout << "func1" << std::endl;
}
void func2()
{
writelock.lock();
//unique_lock<mutex> lock_guard(writelock);
std::cout << "func2" << std::endl;
my_variable.notify_all();
std::cout << "notify_all excute, then sleep!" << std::endl;
Sleep(3000);
writelock.unlock();
}
可以看到起了两个线程之后,func2在notify之后线程1没有立即被唤醒,二十在退出时unlock锁之后线程1才被唤醒。**也就是说wait调用时会有几个动作,如果条件不满足,释放锁,直到获取到条件变量后,就会尝试加锁,如果加锁成功,继续执行,否则阻塞。**所以看到notify的实现,可以在调用LOG函数的地方notify_one()一次。
上面的这段理解好像有问题
参考
实现文件名、行号的获取
系统有两个宏,这两个宏能够获取到输出的文明名和行号
__FILE__ // 文件名
__LINE__ // 行号
__FUNCTION__ // 函数名
实现格式化输出的效果
类似于printf,怎么实现多参数输入?
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
char *make_message(const char *fmt, ...)
{
/* 初始时假设我们只需要不超过100字节大小的空间 */
int n, size = 100;
char *p;
va_list ap;
if ( (p = (char *) malloc(size*sizeof(char))) == NULL)
return NULL;
while (1)
{
/* 尝试在申请的空间中进行打印操作 */
va_start(ap, fmt);
n = vsnprintf (p, size, fmt, ap);
va_end(ap);
/* 如果vsnprintf调用成功,返回该字符串 */
if (n > -1 && n < size)
return p;
/* vsnprintf调用失败(n<0),或者p的空间不足够容纳size大小的字符串(n>=size),尝试申请更大的空间*/
size *= 2; /* 两倍原来大小的空间 */
if ((p = (char *)realloc(p, size*sizeof(char))) == NULL)
return NULL;
}
}
关于int _vsnprintf(char* str, size_t size, const char* format, va_list ap) //这是该函数的函数声明
1.char *str [out],把生成的格式化的字符串存放在这里.
2.size_t size [in], str可接受的最大字符数 [1] (非字节数,UNICODE一个字符两个字节),防止产生数组越界.
3.const char *format [in], 指定输出格式的字符串,它决定了你需要提供的可变参数的类型、个数和顺序。
4.va_list ap [in], va_list变量. va:variable-argument:可变参数
以上是日志打印类的一些前期的准备工作,至于后期如果系统的日志打印需要优化时,再去考虑实际的场景。