上一篇文章中,我们使用gdb抓取堆栈分析了server运行,得出了结论。在某处抢占到读锁(对于共享资源map容器的保护,map的key为设备id,value为该设备的相关信息)时,获取设备相关信息,去进行过滤判断然后进行非常耗时的策略计算等操作,导致了其他线程在获取读锁或写锁时一直处于等待状态无法处理业务。并且总的来说是一个多读少写(相对少写)的情况。
当前项目的处理 伪代码
void dealtask()
{
//并且这个读锁粒度太大了,当然还有其它业务也需要使用这个map,但并不是很费事的操作。不过这里的策略计算是很重要的一环
ReadLock();
//map 过滤... 计算策略...
for(...)
{
map<string,DevInfo>::const_iterator it = devinfo.find(...);
if(it != devinfo->end())
contiue;
if(it->second...)
continue;
dosomething(it->second);
calc();
}
}
//除了上报(上报次数多,每台设备上报一次)可能还有其它部分需要申请写锁
void write()
{
WriteLock();
//更新map
devinfo.insert(...)
}
读写锁代码中调用次数为3:1,但实际上因为每次每个设备上报更新一次就要申请写锁更新map,并且某段时间上报是比较频繁的。对于这一点,每次上报的设备信息加入队列,而非直接更新map。做一个定时器,定时的去从队列中取出这段时间上报的一次性的更新map,来减少写锁的申请。关于为什么这么做,因为我们要去优化这种对map使用读写锁来保证数据安全的方式,使用shared_ptr来实现copy_on_write,第一减少读锁的粒度,计算过滤等操作不在锁范围内完成,去更新map时,写锁用来更新复制出来的map,所以减少写锁的调用次数很重要。
优化后伪代码
using DevMap = map<string,DevInfo>;
using DevMapPtr = shared_ptr<DevMap>;
DevMapPtr g_devinfo;
void read()
{
DevMapPtr devinfo;
{
Lock();
devinfo = g_devinfo;//引用计数+1,相较于第一种读写锁,这里锁的粒度大大减少了,多线程读并发性能很好
}
//以下为策略计算 过滤等等耗时操作,因为使用了shared_ptr,锁的粒度很小了,只是增加了一个引用计数
for(...)
{
map<string,DevInfo>::const_iterator it = devinfo->find(...);
if(it != devinfo->end())
contiue;
if(it->second...)
continue;
dosomething(it->second);
calc();
}
}
void write()
{
map<string,DevInfo> mapdevid2devinfo;
{
Lock_queue();
mapdevid2devinfo.swap(/*队列中的map*/);
//从队列中取出汇总上来的设备信息,减少写锁调用次数
}
Lock();
if(!g_devinfo.unique())//unique返回fasle说明有其它线程在读取g_devinfo,复制一份在副本上修改,为ture时可以直接在原地修改
{
g_devinfo.reset(new Devmap(*g_devinfo));
//do something
//DevMapPtr NewDevPtr(new DevMap (*g_devinfo));
//g_devinfo.swap(NewDevPtr);
//因为这里是有其他线程读的,所以使用reset使引用计数减1也不会释放。g_devinfo指向了新地址,引用计数为1(但内容完全相同)。原来那些引用计数最终归0,会自己释放,不用担心内存泄漏。唯一的缺陷可能时数据不是那么的实时,会有一些的“旧”,但是我们的业务场景是可以接受的
}
assert(g_devinfo.unique());
for(auto &it : mapdevid2devinfo)
(*g_devinfo)[it->first]=it->second;
}
总结:使用shared_ptr,将原来申请读锁时计算费时导致其它读写锁无法继续业务,改为了获取互斥锁,仅仅是引用计数+1,锁的粒度大大减少,使用这个局部的变量去进行耗时的策略计算等。
申请写锁时,判断此时是否有其他线程在处理,没有在原地更新map,有的话去复制一份,在复制出来的map上更新。