2014年(6)
分类: C/C++
2014-09-03 17:05:02
lazy evaluation缓式评估
--需要读取n多配置项,如何加速程序的启动
当你还是小孩的时候,父母要你清理房间的情景,你还记得吗?如果你和我一样,你会说“好”,
然后继续进行手上的动作,你不会清理房间。事实上,清理房间是在万不得已的情况下(通常是
走廊上传来父母的脚步声,他们打算看看你是否真的行动了)才开始行动的。然后你会以百米冲刺
的速度,全速清理房间,如果你够幸运,你的双亲根部没来查看,那么你就不必清理房间,而且
躲过一劫。拖延战术在五岁小孩身上管用,在c++身上一样管用。然而计算机科学里面,这样的
拖延战术有了一个高贵的名称lazy evaluation(缓式评估)。一旦你采用lazy evaluation,
就是以某种方式撰写classes,使他们延缓运算,直到哪些运算结果刻不容缓被迫切需求为止。如果
其运算一直不被需要,运算也就一直不被执行。
以上摘自More Effective C++ Scott Meyers条款17《考虑使用lazy evaluation》。受Scott启发,
发现lazy evaluation恰好可用在楼主的军工项目中。项目可执行程序在刚刚启动时需读取大批量
配置项(程序中有时会使用大型对象,其中内含许多字段,如此对象必须在程序每次执行时保持一致性
与连贯性,所以它们必须存储在文件或者数据库中,各种业务码、线程池个数、数据库连接池个数、
定时器启动与间隔时差等等均属于此范畴,楼主项目中配置项储存在一种变形数据库中),因此可执行
程序的启动相当之慢,慢的很难接受,大概近10秒靠右吧。直到了解到lazy evaluation战术,便找到
解决之道。
程序启动之初,我们在产生这个大的配置项对象时,只产生一个对象的“空壳”,不从数据库中读取任何
字段的数据,当配置项对象的某个字段被需要了,程序才从数据库中读取此字段的内容。假如你在程序的
开发阶段运行(启动)程序只想调试X、Y、Z配置码的XYZ业务,便可轻松躲过读取其他业务的配置码的
烦人时间。当然也有一些天生就是协助性的配置项(像线程池个数、数据库连接池个数)每次都避免不了被
读取的命运,但是我们加速了启动时间。或许程序运行很长时间之后,各种业务的配置码都在在需要的时候
被读取了出来,整个配置项对象的内容均被填充,只不过是把一开始启动的时间分摊给了以后的局部部分,
加起来的总时间甚至超过一开始启动的10秒时间,但是我们加速了启动时间,比如我们大家不能容忍在玩游戏
时还需等待若干的启动时间一样,我想进度条告诉玩家加载了多少,还需加载多少(或者进度条的进度都是
假的也不一定)跟此处对配置项实施lazy evaluation有着异曲同工之妙。
直奔主题:
/*****************************************************************************************/
class config
: private entity
/*entity是boost库中noncopyable的一个简单实现,具体请参考c++ Fixed Allocation内存池博文*/
{
public:
typedef unsigned short ushort;
config()
: intField1(0),/*整形这样的基本类型放到初始化列表或者构造函数内基本一样吧*/
intField2(0),
intField3(0),
...
strField1(),
strField2(),
strField3(),
...
{
/*C风格的数组和C风格的结构体只能放到构造函数里面吧*/
memset(ushortField1, 0x00, 3*sizeof(ushort));
memset(ushortField2, 0x00, 3*sizeof(ushort));
memset(ushortField3, 0x00, 3*sizeof(ushort));
...
}
~config()
/*不作为基类的类不应该声明成virtual析构函数,希望以后能在博文中聊到*/
{
}
/*--------------------------------------------------*/
int set_XYZ()
{
__mutex.lock();
if(0x00 == ushortField1[0])
{
... //某些数据库读取操作
ushortField1[0] = ...;
ushortField1[1] = ...;
ushortField1[2] = ...;
/*
memcpy(ushortField1, ..., 3*sizeof(ushort));
*/
}
__mutex.unlock();
return 0;
}
ushort get_XYZ_X()
{
if(0x00 == ushortField1[0])
set_XYZ();
return ushortField1[0];
}
ushort get_XYZ_Y() const { return ushortField1[1]; }
ushort get_XYZ_Z() const { return ushortField1[2]; }
/*--------------------------------------------------*/
private:
ushort ushortField1[3];
ushort ushortField2[3];
ushort ushortField3[3];
...
int intField1;
int intField2;
int intField3;
...
std::string strField1;
std::string strField2;
std::string strField3;
...
mutex __mutex;
};
/*****************************************************************************************/
有如下注意事项:
(1)get_XYZ_X()和get_XYZ_Y()、get_XYZ_Z()的获取方式不一致
配置码有的有X、Y、Z三部分组成,X可能是区分哪一个分系统,Y可能是区分哪一个业务模块,Z可能是大业务模块
里面的具体业务,又或者X是表示哪一个局域网,Y表示是局域网内的哪一个主机,Z表示是端口号也就是主机上的
哪一应用程序。就是山东省滨州市阳信县的归属关系。一般情况下是XYZ必须一起使用的,就是先使用X再使用Y和
Z。因此在get_XYZ_X内已经获取到数据了,get_XYZ_Y无需操心其他,直接使用即可,因为X是带头大哥呀,X已经
铺好路了。在一些特殊情况下,可能X、Y、Z不会存在使用上的前后顺序关系,因此都必须使用get_XYZ_X的方式,
以确保安全,比如先使用了Z,然后在使用了X,虽然多出执行判断X是否为空的语句,但是在使用Z的时候已经整体
上为X、Y、Z赋值了呀,所以还是省了不少事的。这是有人就会发问,干嘛在使用Z的时候就把X、Y、Z全部给获取
并赋值啊?只给Z赋值,这样在使用X的时候,你就必须再去执行获取并给X赋值的动作了!我还能怎样回答呢?我
只能反问那你干嘛把这种互不相干的东东放在一个数组里呢?分开存储啊!既然放在一起必然就是有关系有原因的。
(2)只对写进行了加锁,对读并没有加锁
因为imperfect c++里的一组相关数据给出了明证,人为强制或者程序运行时间很长致使加锁解锁次数达到某个数值时,
加解锁耗时是函数调用耗时的150倍。所以将对读写加锁保护,改成了只对写进行加锁。但是紧接着又会面临另一个
问题:
if(0x00 == ushortField1[0])
set_XYZ();
假如线程A和线程B在某一特定时刻均需获取这个业务码。于是有很大的几率出现线程A执行条件语句时发现为真,
紧接着cpu并没有安排线程A去直接执行set_XYZ(),而是安排线程B去执行条件语句,结果很简单,线程B也获得了
执行set_XYZ()的权利。于是乎假如set_XYZ()方法里面没有if(0x00 == ushortField1[0])这个再次的条件判断的
话,线程A和线程B均向数据库索取了两次X、Y、Z。楼主就是遇到过这个情况,于是紧锣密鼓的补救行动展开了,
发现了在set_XYZ()方法里面再加一层if(0x00 == ushortField1[0])条件判断即可顺利解决,只不过是浪费掉了
一次线程B加锁+执行条件判断+解锁的时间,但是换来了安全,同时也避免了逻辑的漏洞。但是仔细想想假如线程B是
在线程A获取完了X、Y、Z之后去使用X、Y、Z的话,加锁+执行条件判断+解锁的时间也不会浪费啊。如果你非要说
在线程A获取X、Y、Z时出现一个线程C去捣乱,去浪费加锁+执行条件判断+解锁的时间,我还能咋办?就让他去干吧!
ps:后来在研读《head first 设计模式》时在单件模式里面遇到了这个双重判断的机制,^_^。
(3)锁的使用
set_XYZ()里面加入在从数据库读取内容时报错必须提前return或者出现异常(不想就地消化异常)必须抛出异常,
那么就要在哪之前进行解锁操作,可是在后续的维护中还是有几率出现忘记在提前退出之前进行解锁操作的,最烦人
的死锁问题出现了,必定需耗费(巨大)心力去寻找根源,那如何避免死锁呢?muduo源码里面的锁守卫即可解决,
具体请参考c++ Fixed Allocation内存池博文
(4)其实是不想提volatile的,但是总感觉一谈到线程间共享的数据,volatile就会映入脑海,volatile就是
告诉编译器不要优化我,在读时不要去寄存器中寻找我的备份,直接到内存中读。此处没有用volatile修饰时因为
在编译的时候根本没有打开优化开关,而且volatile修饰string类型时会有很多问题的,不能访问size、length等
成员函数等等。
(5)mutable
当你企图在const member functions内修改data members,编译器不会同意的,除非你用mutable修饰data members
,即用mutable修饰的data members可以在任何member functions内修改,甚至是const member funcitons。
lazy evaluation的一个典型例子就是reference counting(引用计数)在某些string里面的应用。例如:
std::string s1 = "hello world";
std::string s2 = s1;
string copy构造函数的一个常见的做法是为s2去申请空间并将“hello world”存放其内,然而有的做法是采用lazy
evaluation的方式,让s2分享s1的内容,而不再给予s2一个“s1的副本”,只需要简单地记录下和谁共享了写东西即可,
这样便节省了申请空间+复制东西的成本。这样对于只读数据而不是写入数据并不会有任何影响。假如s2在程序中并没有
任何写入动作的话,我们真可以庆幸躲过了一劫。然而在需要给s2写入数据的时候,我们必须为s2做出一个s1的副本出来,
并且使它成为s2的专属数据。还是那句话:在你真正需要之前,不要着急为某物做任何副本或者赋于真实数值,取而代之
的是--用拖延战术对付它。