第101讲 Logger

好吧,很久都没说我们的C++的了,最近确实是忙,嗯,这两天程序基本写得差不多,接下来的事就是各种测试,于是忙里偷闲(其实这算是在忙吧),来说说我们的C++。
记得在很久的上一讲里面我们说的是一个基于OpenGL的一个3D学习框架,现在回头去讲可能大家都忘记的差不多,我也好久没去碰那框架了,于是干脆来说说一些新的东西,这一讲里面我们来看看怎么实现一个日志记录系统。
关于这东西,大家应该马上想到就是写文件,嗯……确实,我们这里也是在说写文件的事,不过我们可能会遇到一些情况,那就是我们要记录的东西太多,但是我们又不想因为记录日志而让程序损失运行效率,所以我们可能会将日志扔到后台去记录——这就是我们今天要说的多线程。
关于多线程,如果大家还有影响的话,我们在前面的章节中似乎说提到过,不过我们今天说的多线程和以前说的是略有区别的,前面的章节中我们是使用Api来开线程,而这一讲里面我们不再使用API函数,而使用C++11标准的thread库。


//======================================================
//class Logger
#pragma once
#pragma warning(disable:4996)
#include <string>
#include <
fstream>
#include <cstdarg>
#include <cstdio>
#include <thread>
#include <mutex>
#include <list>
#include <condition_variable>
#include <memory>

#ifdef _WIN32
#include <Windows.h>
#endif

typedef std::thread

Thread;


typedef std::mutex

Mutex;


typedef std::recursive_mutex

Recursice_Mutex;


typedef std::condition_variable

Condition_Variable;


typedef std::unique_lock<std::mutex> Unique_lock;

class Logger;



Logger&

TheLogger();

class Logger{

public:

Logger();


virtual ~Logger();

static std::shared_ptr<Logger>Instance(){
static std::shared_ptr<Logger>data = std::shared_ptr<Logger>(new Logger);


return data;



}

void EnableConsole(bool useConsole);


void SetLogFile(const std::string&

);


void log(const std::string&

);


void log(char*,…);



void isNeedTime(bool isTime);



protected:

void ProceMsg();


void ShowConsole();


void pushMsg(const std::string&

);


std::string

m_logfile;


std::ofstream

m_outFile;

bool

b_Exit;


bool

b_ThreadStart;


bool

b_useConsole;


bool

b_ThreadConsoleStart;


bool

b_isConsloe;


bool b_is_need_time;


std::list<std::string> m_queue;


std::list<std::string> m_queue_to_console;


Thread

m_thread;


Thread

m_thread_console;


Mutex

m_mutexlog;


Mutex

m_mutexconsole;


Condition_Variable
m_condition;


Condition_Variable
m_conditionconsole;



private:

Logger(const Logger&

);


Logger&

operator=(const Logger&

);


};

//=====================================================

这就是我们所需要的一些功能,那么我来说一下这些接口的功能,首先这是一个禁止复制和赋值的类,因为我们将这两个函数声明为private,所以TheLogger()其实就是创建一个单例的,什么叫单例呢?就是说如果你使用这个函数来构造Logger对象,那么这个对象在你的程序中只会有一个,再直接一些就是另类的全局对象。
和TheLogger()一样,成员函数Instance()同样是为了构造一个单例的,不过这是使用智能指针来管理对象,所以这两种方式构造出来的对象都不需要我们自己维护他的生命周期,所以当我们想要一个全局logger的时候,我们可以考虑使用这两种方式来构造。
EnableConsole(bool)该函数的功能是开启控制台打印功能,当然如果我们传递的参数是true的话,那么如果我们当前的应用程序控制台程序就直接打印在控制台上,如果是窗口程序则会开启一个控制台窗口,然后将信息打印在控制台上,这有利于程序的调试(我就是这么干的,在窗口程序开启控制台来输出程序运行信息,在正式发布的时候将控制台功能关闭即可)。
SetLogFile()顾名思义该函数是设置一个log文件,log信息就是写进这个文件里面。
log()重载的两个log函数当然就是所需要记录的log信息了,log(char*,…)该函数使用的时候可以像C语言里面的printf一样使用,灵活性极高的一个函数,
void log(const std::string&

)该函数可以完全被上一个取代,当然他也有他自己存在的价值的了。
isNeedTime(bool isTime)该函数通常是用来禁用时间信息的,因为在默认情况下是开启时间功能的,那么这个时间功能是什么呢?当然就是在记录着当前log的时间点。
ProceMsg()处理消息的线程函数,该函数将信息写进log文件中,ShowConsole()控制台线程函数,这两个函数分别在不同的线程中跑,所以不会因为谁快谁慢而拖累对方。
到这里几个函数接口都已经给大家解释了,剩下的一些数据成员这里还是简单的介绍一下:

m_logfile : log文件名
m_outFile : 写log信息的文件流
b_Exit
: 控制log线程的退出
b_ThreadStart : log线程是否开启
b_useConsole : 是否使用控制台
b_ThreadConsoleStart : 控制台线程是否开启
b_isConsloe : 是否是新开启的控制台(主要是在窗口程序中使用)
m_queue : log信息队列,本来可以使用deque的,但后来还是使用list
m_queue_to_console : 控制台消息队列
m_thread : log线程
m_thread_console : 控制台线程
m_mutexlog : log锁
m_mutexconsole : 控制台锁
m_condition : log条件变量
m_conditionconsole : 控制台条件变量

//======================================================
这些数据成员想必大家都已经很清楚了,那么我们接下来来看看如何实现
//======================================================
//Logger.cpp

#include "

Logger.h"



#include <iostream>

#include <ctime>

#include <iomanip>


Logger&

TheLogger(){
static Logger Log;


return Log;



}
//==============class Logger======================

Logger::Logger() :b_Exit(false), b_ThreadStart(false), b_is_time(true),
b_ThreadConsoleStart(false),{
#ifdef _WIN32
HANDLE hTest = GetStdHandle(STD_OUTPUT_HANDLE);


b_isConsloe = (hTest != nullptr);


#endif

m_thread = Thread(&

Logger::ProceMsg, this);


Unique_lock lock(m_mutexStart);


m_conditionStart.wait(lock, [&

](){
return b_ThreadStart == true;

});


b_ThreadStart = false;



}

Logger::~Logger(){
b_Exit = true;


m_condition.notify_all();


m_thread.join();

//等待日志线程返回
if (b_useConsole){
m_conditionconsole.notify_all();


m_thread_console.join();

//等待控制台打印线程返回

}

}

//是否启用控制台打印消息

void Logger::EnableConsole(bool useconsole){
b_useConsole = useconsole;


if (b_useConsole){
m_thread_console = Thread(&

Logger::ShowConsole, this);


Unique_lock lock(m_mutexStart);


m_conditionCosoleStart.wait(lock, [&

](){
return b_ThreadConsoleStart == true;

});


b_ThreadConsoleStart = false;



}

}


//设置log文件,如果当前路径不存在,则抛出异常

void Logger::SetLogFile(const std::string&

filename){
if (filename.empty())
return;


m_logfile = filename;


if (m_outFile){
m_outFile.close();



}

m_outFile.open(m_logfile, std::ios::app);


if (m_outFile.fail()){
throw std::runtime_error("

Unable Open File for Write!!!!"

);



}

}


void Logger::log(const std::string&

enterque){
Unique_lock lock(m_mutex);


Unique_lock lock2(m_mutexconsole);


pushMsg(enterque);



}

//使用变长参数模式来接收消息

void Logger::log(char* str, …){
Unique_lock lock(m_mutex);


Unique_lock lock2(m_mutexconsole);


char buf[40960] ={ 0
};


va_list ap;


va_start(ap, str);


vsprintf(buf, str, ap);


va_end(ap);


pushMsg(std::string(buf));



}


void Logger::isNeedTime(bool isTime){
b_is_time = isTime;



}


void Logger::pushMsg(const std::string&

str){
if (b_useConsole){
m_queue_to_console.push_back(str);


m_conditionconsole.notify_all();



}
m_queue.push_back(str);


m_condition.notify_all();



}

//日志记录线程

void Logger::ProceMsg(){
Unique_lock lock(m_mutex);


b_ThreadStart = true;


while (b_ThreadStart){
m_conditionStart.notify_one();



}

while (1){

m_condition.wait(lock);


lock.unlock();


while (1){

lock.lock();



if (m_queue.empty()){

break;


}

else{

time_t tt;



time(&

tt);



tm t;



localtime_s(&

t, &

tt);



if (!m_outFile){

m_queue.clear();



break;


}

if (b_is_time){

m_outFile <<std::put_time(&

t, "

%c"

) <<"

"

<<m_queue.front() <<std::endl;


}

else{

m_outFile <<m_queue.front() <<std::endl;


}

m_queue.pop_front();


}

lock.unlock();



}
if (b_Exit)

break;



}
m_outFile.close();



}

//控制台线程

void Logger::ShowConsole(){
Unique_lock lock(m_mutexconsole);


if (!b_isConsloe){

int res = AllocConsole();


if (res == 0){

char* buffer = new char[1024];



memset(buffer, 0, 1024);



sprintf(buffer, "

Error:Unable open Console for print Msg and Error ID:%d"

, GetLastError());



std::cerr <<buffer <<std::endl;

;



}
freopen("

CONOUT$"

, "

w+t"

, stdout);


freopen("

CONIN$"

, "

w+t"

, stdin);



}
b_ThreadConsoleStart = true;


while (b_ThreadConsoleStart){
m_conditionCosoleStart.notify_one();



}

while (1){

m_conditionconsole.wait(lock);


lock.unlock();


while (1){

lock.lock();



if (m_queue_to_console.empty()){

break;


}

else{

time_t tt;



time(&

tt);



tm t;



localtime_s(&

t, &

tt);



std::cout <<std::put_time(&

t, "

%c"

) <<"

"

<<m_queue_to_console.front() <<std::endl;



m_queue_to_console.pop_front();


}

lock.unlock();



}
if (b_Exit){

if (!b_isConsloe)

FreeConsole();



break;



}

}

}


//==========================================================
实现其实不难,不过有些地方具有技巧性:
首先我们在构造函数里面开启线程,m_thread = Thread(&

Logger::ProceMsg, this);

这句代码将ProceMsg()作为线程函数来跑,这没什么,但是如果程序本身就只运行一个很短的时间,那么我们可能线程还没开启程序就已经结束,结果就是我们可能得不到我们想要打印的信息,所以我们不只是在构造函数中开启线程,还应该等到线程启动好后再完构造,那么这里就只要让主线程睡下来,m_conditionStart.wait(lock, [&

](){
return b_ThreadStart == true;

});

这句话的意思就是让主线程等待,等待什么呢?就是要等到b_ThreadStart变成true同时和一个通知,那么b_ThreadStart初始化时false的什么时候会被置true呢?ok,我们来看看ProceMsg函数,因为这就是我们的线程函数,在ProceMsg函数的开头就有b_ThreadStart = true,所以当程序走到这里的时候,这个变量就被置true了,也表示线程已经开启,接下来就是通知主线程:
while (b_ThreadStart){
m_conditionStart.notify_one();



}
大家可能会想为什么会是循环通知呢?这里可以告诉大家,如果大家一不小心可能会留下一个极大的bug,那就是可能有时候我们程序启动之后就感觉死机了,为什么呢?因为主线程一直在等待线程的一个通知,但是线程可能一启动就会马上通知主线程,但是主线程可能一时还没反应过来,所以线程通知完成之后就往下走,但是主线程会一直等待线程的通知,但是这时候这个通知就会一直等不到了,所以程序就会出现像是死机的样子,确实也是死了,因为没有接受到线程的通知就主题走不下去,当然我们可以使用超时来完成但是这里我们并没有这么做,因为超时不一定表示成功,所以这里我们使用循环通知,因为当主线程等到通知时,变量b_ThreadStart会被置为false,从而打破线程的循环通知。
对于控制台线程同理,大家可以注意ShowConsole()的开始部分,这是在窗口程序中开启控制台的代码,同时这条线程是在EnableConsole()设置true作为才会开启的。
ok,这个小工具在开发项目的时候还是挺有用的,今天就这么多吧。

第101讲   Logger


原文始发于微信公众号(

C/C++的编程教室

):第101讲 Logger

|

发表评论