项目场景
众所周知,C++的STL里提供的map容器不能保证线程安全,项目因此给map加了一个锁,封装了一个线程安全的map,如下所示
RWLock gRwMutex;
struct ConcurrentMap {
public:
void insert(const std::string& name, int age) {
WriteGuard lock(gRwMutex);
ageMap.insert({name, age});
};
void erase(const std::string& name) {
WriteGuard lock(gRwMutex);
ageMap.erase(name);
};
void clear() {
WriteGuard lock(gRwMutex);
ageMap.clear();
}
private:
std::map<std::string, int> ageMap;
};
这里的读写锁是全局资源,通过封装RAII类,在构造函数内实现读或写保护,析构函数内实现解锁,就不用担心锁忘记释放,看起来也更简洁【参见Effective C++条款13以对象管理资源】
问题描述
用上述ConcurrentMap创建了一个全局的map,供多线程使用,运行测试用例时,内存检测工具定位到这个类存在double free的问题
原因分析
double free,表明堆上的指针存在重复释放,应该是这个类里包裹的map内的对象被重复释放了,分析认为该类并没有自定义一个析构函数,默认的析构函数线程不安全
解决方案
- 错误方案一
添加如下析构函数
struct ConcurrentMap {
~ConcurrentMap() {
WriteGuard lock(gRwMutex);
}
};
分析:实践证明,在析构函数内加锁,并不能锁住内置map对象的析构,还是会出现double free,map的资源释放应该在析构函数执行之后
- 错误方案二
添加如下析构函数
struct ConcurrentMap {
~ConcurrentMap() {
this->clear();
}
};
分析:这里想要在析构时手动释放map资源,但this->clear()底层调用的是map原生的clear方法,而这个方法并不会释放map的内存
- 最终方案(供参考)
使用swap技巧代替clear
struct ConcurrentMap {
void clear() {
WriteGuard lock(gRwMutex);
std::map<std::string, int> emptyMap;
emptyMap.swap(this->ageMap);
}
~ConcurrentMap() {
this->clear();
}
};
分析:使用swap技巧,在map自己析构前手动释放map的内存【参见Effective STL 第17条 使用"swap"技巧除去多余容量】
其他思考
想要写一个并发安全的析构函数,最好要避免类内堆上资源重复析构,使用swap方法提前释放的方法并不那么通用
通用方法是使用引用计数,或者使用C++内置的智能指针,因此更好的设计应该是这样的
RWLock gRwMutex;
struct ConcurrentMap {
using AgeMap = std::map<std::string, int>;
void insert(const std::string& name, int age) {
WriteGuard lock(gRwMutex);
spAgeMap->insert({name, age});
};
void erase(const std::string& name) {
WriteGuard lock(gRwMutex);
spAgeMap->erase(name);
};
void clear() {
WriteGuard lock(gRwMutex);
spAgeMap->clear();
}
private:
std::shared_ptr<AgeMap> spAgeMap= std::make_shared<AgeMap>();
};
这样设计的好处有以下几点:
- 当类被析构的时候,由智能指针来管理我们实际要访问的ageMap,我们不避手动释放内存
- 避免了在析构函数内使用锁
- 如果锁资源是在类内(仅供该类对象使用),那么在析构函数内是不能加锁的,不然可能存在锁在一个线程内被析构,另外一个线程再也无法获取锁资源,发生未知错误
- 而且如果有继承存在,考虑到子类资源会先于父类资源释放,如果锁在子类里面,就会存在锁提前失效的问题
此外,我们设计的这个类只用来定义一个全局唯一的变量,应该设计成单例模式,但目前这个类的设计还存在很多优化的地方(如将构造函数设为私有,仅对外提供静态接口),不是此文的重点不多描述了