C++并发场景下的double free

项目场景

众所周知,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,我们不避手动释放内存
  • 避免了在析构函数内使用锁
    • 如果锁资源是在类内(仅供该类对象使用),那么在析构函数内是不能加锁的,不然可能存在锁在一个线程内被析构,另外一个线程再也无法获取锁资源,发生未知错误
    • 而且如果有继承存在,考虑到子类资源会先于父类资源释放,如果锁在子类里面,就会存在锁提前失效的问题

此外,我们设计的这个类只用来定义一个全局唯一的变量,应该设计成单例模式,但目前这个类的设计还存在很多优化的地方(如将构造函数设为私有,仅对外提供静态接口),不是此文的重点不多描述了

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值