本来想接着写LogQueue,但发现LogQueue可以直接看做一个黑箱,只要知道它提供的是LogItem的队列功能就可以了。还是先讲LogManager比较好理解。
因为希望每一个需要log的程序模块,都能有一个单独的LogSystem,该模块的LogSystem中又能管理多个线程的log。假设我们已经做好了一个LogManager类,实现了log的功能。那么我们想在项目的网络模块和GUI模块中使用它,当然可以通过在这2个模块中分别定义:
LogManager logmgr1; //网络模块中的log
LogManager logmgr2; //GUI模块中的log
但这么写不太好,因为直接声明LogManager的实例对象在每个模块中不优雅,实例名可能会冲突,而且LogManager实际上是独立于特定模块的,在特定模块中声明它的实例显得有些逻辑的缺憾。
因此我们通过2个手段解决这些问题:
1 对于模块内作用域问题,我们需要实现一个Singleton即单例模式,于编译期即建立一个全局的LogManager。这样我们就无需在特定模块声明一个log类对象来实现log功能了。
2 对于如何区别不同模块间的LogManager,可以给LogManager传入一个int型的模板参数,于是其模板实例即构成了不同模块的LogManger:
template<int ModuleID>
class LogManager {};
通过传入枚举值,如LogManager<1>,LogManger<2>即可以生成不同的模板实例。传入该模板参数的一个潜在好处是我们也许可以根据特定模块做一些配置上的调整。
因为身处多线程环境,因此LogManager需要支持线程锁。我使用的是Loki库中一个优雅的线程锁实现,它包含在threads.h文件中。Loki的提供了2种类型的线程锁。一种是针对整个类的线程锁:
template <class Host, class MutexPolicy = LOKI_DEFAULT_MUTEX >
class ClassLevelLockable {};
使用该锁将导致所有该类对象被锁定。LogManager因为实现单例模式,使用ClassLevelLockable锁顺理成章。
另外一个是对象的线程锁,其作用域范围只局限在该类的特定对象中。LogQueue使用该锁。
LogManager作为Log System的包装器,显然需要提供对Log System的各种配置的设置。一些设置可以在编译期设定(通过一些编译条件和宏),而一些可以在运行期设定(需要读配置文件)。
为方便,对于Log System中的LogItem的字符串buffer大小,和LogQueue的初始队列长度,我们通过LogManager的模板参数控制:
template<
int ModuleID,
size_t ItemSize_ = LOG_ITEM_SIZE,
size_t QueueSize_ = LOG_QUEUE_SIZE,
template<size_t> class ItemType_ = LogItem,
template<int, class, size_t> class QueueType_ = LogQueue,
template <class, class> class ThreadingModel = LOKI_DEFAULT_THREADING_NO_OBJ_LEVEL,
class MutexPolicy = LOKI_DEFAULT_MUTEX
>
class LogManager {};
如上LOG_ITEM_SIZE和LOG_QUEUE_SIZE的默认值为128。
于是通过定义:
typedef LogManager<1> MyLogManager;
MyLogManager就是一个表示用于模块1中,默认LogItem的字符串长度为128,LogQueue默认长度为128,默认使用ClassLevelLockable线程锁的LogManager模板实例。
为了实现如下语句:
logx(1)<< i << " threadfunc1_in_module1 " << " 11\n";
该语句在_DEBUG模式下将跳过后边的输入,这样就节省了函数调用和参数输入的开销。于是logx(1)最好不要用函数实现。即使你在函数内部设了分支语句来判断是否输出,但实际上还是隐含了该函数的调用开销特别是传入的开销(虽然它们没用到)。
《游戏编程精粹3》的LogSystem找到这样一个手法,定义一个宏:
#define LOG(type) (type) && (type)
比较巧妙的绕开了这个问题。宏中的type可以是一个LogManger类型或者引用。
该宏的应用值得好好研究。
我们将重载bool操作符,在&&前的第一次向bool的转换中,判定log标志是否为真以及是否发送缓冲区的内容到LogQueue中。而&&之后,我们看下面的展开就明白了:
LOG(LogManager<1>::Instance()) << "hello";
///////////////////////////////////////////////////
LogManager<1>::Instance() && ( LogManager<1>::Instance() << "hello"; )
一旦第一个LogManager<1>::Instance()向bool的转换为假,则编译器不会处理后边的语句。于是我们就省下了函数调用和参数压栈的开销。
operator bool ()
{
return m_flag && sendBuffer();
}
m_flag可以用宏来赋值或定义一个ConfigInitializationPolicy在执行期赋值。
LogManager需要单开一个线程遍历QueueGroup,完成对item的输出。为了每一个线程对应一个queue,定义了一个map<int, QueueType>。map也可以使用hash或优先队列等。
typedef map<int, QueueType*> QueueGroup;//暂时用map
key为线程的ID值。
最后重载流输出操作符<<:
template<class T>
MyLogMangerType& operator << (const T& t)
{
Lock guard;
QueueType* queue = getQueue( GetCurrentThreadId() );
assert(queue);
(*queue) << t;
return *this;
}
基本上LogManager的设计如此。
原来LogManager中定义了一个ItemType即LogItem类型的buffer作为缓冲区,但后面发现因为logx(type)这个宏不是一个原子操作,在多线程下会导致问题,没有想到好的解决方法(都会使写法不流畅)。
如果我们从外部传入线程ID,则应该可以避免每次调用进行时锁定的代价,目前不实现这个做法。
LogManager的代码:
#ifndef __LogManager__H__
#define __LogManager__H__
#include "stdafx.h"
#include "LogItem.h"
#include "LogQueue.h"
#include "LogConfigMacro.h"
#include "LogOutputPolicy.h"
#include "loki/Singleton.h"
#include "loki/Threads.h"
using namespace std;//为示例方便,直接使用std名字空间
template<
int ModuleID,
size_t ItemSize_ = LOG_ITEM_SIZE,
size_t QueueSize_ = LOG_QUEUE_SIZE,
template<size_t> class ItemType_ = LogItem,
template<int, class, size_t> class QueueType_ = LogQueue,
template <class, class> class ThreadingModel = LOKI_DEFAULT_THREADING_NO_OBJ_LEVEL,
class MutexPolicy = LOKI_DEFAULT_MUTEX
>
class LogManager
{
public:
typedef ItemType_<ItemSize_> ItemType;
typedef QueueType_<ModuleID, ItemType, QueueSize_> QueueType;
typedef LogManager<ModuleID, ItemSize_, QueueSize_,
ItemType_, QueueType_,
ThreadingModel, MutexPolicy> MyLogMangerType;
typedef Loki::SingletonHolder<MyLogMangerType> MySingletonLogHolder;
typedef map<int, QueueType*> QueueGroup;//暂时用map
typename typedef QueueGroup::iterator GroupIter;
typename typedef ThreadingModel<MyLogMangerType, MutexPolicy>::Lock Lock;
private:
QueueGroup m_queueGroup;
bool m_flag; //若需要从配置文件中读取,暂不实现
public:
explicit MyLogMangerType(): m_flag(IS_NEED_LOG)
{
runLogThread();
}
public:
operator bool ()
{
return m_flag && sendBuffer();
}
bool sendBuffer()
{
Lock guard;
GroupIter iter;
for(iter = m_queueGroup.begin(); iter != m_queueGroup.end(); iter++)
{
(*iter).second->dispatchBuffer();
}
return true;
}
QueueType* getQueue(DWORD id)
{
//Lock guard;
GroupIter iter = m_queueGroup.find(id);
if(iter != m_queueGroup.end())
return (*iter).second;
else
{
QueueType* newQueue = new QueueType(id);
m_queueGroup.insert(QueueGroup::value_type(id, newQueue));
return newQueue;
}
}
template<class T>
MyLogMangerType& operator << (const T& t)
{
Lock guard;
QueueType* queue = getQueue( GetCurrentThreadId() );
assert(queue);
(*queue) << t;
return *this;
}
MyLogMangerType& print(const char* strFormat, ...)
{
va_list args;
char str[ItemSize_];
va_start(args, strFormat);
vsprintf_s(str, strFormat, args);
Lock guard;
QueueType* queue = getQueue( GetCurrentThreadId() );
queue->pushString(str);
return *this;
}
inline static MyLogMangerType& Instance()
{
return MySingletonLogHolder::Instance();
}
void pumpQueueGroup()
{
Lock guard;
GroupIter iter;
for (iter = m_queueGroup.begin(); iter != m_queueGroup.end(); iter++)
{
(*iter).second->pumpQueue();
}
}
~LogManager< ModuleID, ItemSize_, QueueSize_,
ItemType_, QueueType_,
ThreadingModel, MutexPolicy >()
{
QueueType* queue;
GroupIter iter;
for (iter = m_queueGroup.begin(); iter != m_queueGroup.end(); iter++)
{
queue = (*iter).second;
assert(queue);
delete queue;
}
m_queueGroup.clear();
}
//简单的log线程,测试
static unsigned int WINAPI threadFunc(void* lpParam)
{
MyLogMangerType* pLogMgr= static_cast<MyLogMangerType*>(lpParam);
for(;;)
{
pLogMgr->pumpQueueGroup();
Sleep(100);
}
return 0;
}
void runLogThread()
{
unsigned int id= 0;
_beginthreadex(NULL, 0,MyLogMangerType::threadFunc, (LPVOID)this, 0, &id);
assert(id);
}
};
#endif