本文解决如下几个问题:
- 如何实现一个线程安全的容器,以及这个线程安全的容器什么时候是不安全的;
- 构造函数中,为保证线程安全禁止做哪些事情。
- 析构函数中不宜使用锁的原因。
- 使用指针时该如何判断指针是否还存活?
- 使用锁会降低程序的效率,使得并行的程序串行化,如何减少锁争用造成的延迟。
- shared_ptr的使用技巧与坑;
- 对象池中对象关系的探讨:如何降低对象之间的相互依赖。
- std::bind与std::function的简单理解。
1. stl中的容器大部分都不是线程安全的,如何将其变为线程安全呢?
解决方案:使用锁保护数据成员:
class Array {
private:
mutable std::mutex lock; //注意mutable关键字;
std::vector data;
}
成员函数的实现使用锁保护data成员。
问题:C++中指针访问Array时是不能保证线程安全的。
理由:C++的析构函数的存在,使得其他线程delete掉Array*时,其他线程还阻塞在lock中,析构函数完成,lock就不存在了,阻塞在lock的线程就出现了未定义行为。
2. 构造函数中,为保证线程安全禁止做哪些事情。
- 不能在构造函数中注册回调。
- 不要在构造函数中把this传递给跨线程对象;
- 即使在构造函数最后一行也不行。
解释:
- 在构造函数中注册回调的含义是:将自己的指针保存到容器Observable中,一旦有事件发生,就会调用自己的方法。
Foo(Observable* s) {
s->register(this);
}
this传递出去后,会导致有可能当前对象没有构造完成就调用了成员方法,未定义行为。
- 与上一点相同。感觉含义很类似。
- 最后一行也不行是因为,当前类可能是父类,子类的对象依然没有初始化完成,导致未定义行为。
3. 析构函数中不宜使用锁的原因。
(1)调用析构函数的时候,正常逻辑来说这个对象已经没有其他线程在使用了,用锁也没有效果;
(2)即使使用了锁,析构函数抢到了锁,其他线程还在等待这个锁,析构函数中锁被析构掉了,其他线程就是未定义行为。
4. 使用指针时该如何判断指针是否还存活?
例子:以观察者模式为例,observer对象注册自己到Observable,后者保存有前者的指针,一旦某个事件发生,Observable就通过observer指针调用其成员方法。多线程情况下,Observable无法得知当前调用的observer指针是否还有效,即使使用锁也不行。
方案:需要一种方法能告诉Observable,observer指针是否存活的方法。什么都不做是不可能实现的,需要额外一个变量来表示变量是否存活,可以理解为是指针通过一个代理来访问实际内存,代理掌握了实际内存是否被释放的消息。
实现:本质就是shared_ptr与weak_ptr的实现。如果使用weak_ptr保存指针,可以清楚的知道指针是否存活:如果weak_ptr可以转化为shared_ptr,证明指针还有效,否则无效。
weak_ptr不增加引用计数,weak_ptr对象只能由shared_ptr/weak_ptr赋值构造而来。
不打算决定对象的生死,就使用weak_ptr管理对象指针;否则使用shared_ptr
vector> x;
lock.lock();
for(auto xx : x) {
shared_ptr obj(xx->lock());
if(obj) {
obj->update();
}
else {
x.erase(xx);
}
继续:即使能判断指针是否存活,即不会存在使用已经销毁或者正在销毁的指针了!但是不代表没有其他问题:
(1)锁争用造成的延时;
(2)死锁。
5. 使用锁会降低程序的效率,使得并行的程序串行化,如何减少锁争用造成的延迟。
锁争用:访问需要加锁的数据成员的代码都需要加锁,使得比较简单的函数也需要等待较长时间。
解决1:解决锁争用的方法是:尽量减少临界区的大小;
解决方法1:local copy的方式。(适用于拷贝代价不大的对象)
读操作,临界区内拷贝出来,临界区外使用副本读取;
写操作,临界区外定义副本,完成要完成的操作,临界区内直接赋值或者swap;
写操作,临界区内拷贝出来,临界区外副本操作,临界区内swap;这样会有问题,可能会覆盖其他线程的修改。
写操作如果操作的是shared_ptr,可能会造成shared_ptr保存对象的析构操作(原来shared_ptr对象被赋值了,且引用计数为1),此时析构操作也是在临界区内。
写操作时析构移除临界区:临界区外定义副本,完成要完成的操作,临界区内swap;(此时析构移到了临时对象的身上)
6. shared_ptr的使用技巧与坑
坑:
- shared_ptr会延长对象的生命周期。
解释:某些函数实参采用非引用类型shared_ptr类型,调用这个函数的时候就会发生shared_ptr的拷贝操作,使得对象指针的引用计数值变大。如果这个函数返回一个对象,这个对象也中也存在这个shared_ptr的指针,那么shared_ptr对象的声明周期就被延长了。
例子:std::bind函数,基本作用是,为一个函数指针提供默认参数。其实参就会被拷贝一份出来。(模板参数,不论什么类型都会发生拷贝行为)
shared_ptr的拷贝代价比指针要大。
解释:毕竟还要保存引用计数等变量,修改引用计数等行为。(建议使用引用传递)不能同时使用两个shared_ptr,容易引起误会;
类内(成员函数)使用shared_ptr与类外使用shared_ptr同时使用时,会造成析构两次的问题。
解释:类内部使用share_ptr的需求可以使用:shared_from_this() 代替this;
使用技巧:
- 作为函数参数时,建议使用const reference传递;
- 在创建shared_ptr对象时,可以手动指定析构函数,这样可以保证可以跨dll来删除。
解释:windows下的进程会有好几个堆,每个dll都会有一个堆,一个堆里申请的需要在这个堆释放,所以存在跨模块释放的问题;shared_ptr通过指定析构函数,使得释放时,可以释放对应堆的对象。 - shared_ptr的析构如果可能发生在关键进程,可以用一个专门的线程来处理析构,使用BlockQueue
来转移对象到析构线程; - ower持有指向child的shared_ptr,child持有指向ower的weak_ptr;
解释:ower可以决定对象的生死,child只负责使用,不符合对象的生死。
7. 对象池中对象关系的探讨:如何降低对象之间的相互依赖。
场景:A类中包含了B类对象,对象池的话就是A类中包含了很多B类对象。B类对象可以是暂存在A类中的,用于回调;也可能是被A类所使用。
(1) 需求:A类中的B类对象如果不使用了及时释放掉,以节省内存。
解决方案:不使用了的概念是没有线程在使用了,可以使用指针来保存B类对象,shared_ptr来保存,使得引用计数为0时就释放掉。显然,使用shared_ptr的话,对象永远不会被释放掉。所以使用weak_ptr来保存。
class Item {};
class Factory {
private:
std::map> data_;
mutable std::mutex lock_;
public:
shared_ptr- get(const std::string& key); //使用shared_ptr作为返回值,因为出去使用的对象认为不能随便释放掉。
};
get方法的实现就相当较为简单了:
shared_ptr- Factory::get(const string& key) {
shared_ptr
- ret;
lock_.lock();
auto itemptr = data_[key].lock(); //即使不存在,itemptr也是合理的weak_ptr
if(! itemptr) {
ret.reset(new Item());
data_[key] = ret; //第一,weak_ptr只能由shared_ptr/weak_ptr赋值而来。 第二,两次查找map,效率不高,可以使用引用保存第一次查找的结果。
}
lock_.unlock();
return ret;
}
shared_ptr与weak_ptr使得对象可以被及时释放。
(2)需求:资源的及时释放,保存有weak_ptr的对象也要及时清理掉内存,如何处理。
创建了一个Item给外部使用,保存在data_中以便不要重复创建;但是外部用完了,对象就自动销毁了,但是Factory还保存着资源的weak_ptr,没有意义了。要清理掉。
即:对象的析构不仅仅需要释放自己,还要处理保存有自己weak_ptr的对象
解决方案:使用shared_ptr定制的析构函数来处理。
void deleteItem(Item* item, Factory* factory) {
factory->deleteItem(item->key()); //key是从Item类中获取。
delete item;
}
(3)问题:定制析构需要只能有一个参数,且参数应该是传到shared_ptr中的对象指针,那现在要处理factory,多了一个参数,要咋处理呢?
解决方案:std::bind函数缩减函数参数
auto deleter = std::bind(deleteItem, _1, this); //普通函数作为第一个参数,不需要使用&,静态成员函数与成员函数使用时需要。
这里的this只是一个例子,代表是Factory* 就可以了,因为前面shared_ptr在Factory中构造而来,所以使用this。
(4)指针是不能随便出现的,出现了就会存在内存释放问题,也存在指针是否是野指针的问题。上面的deleteItem函数不合理。
解决:由于在函数内部无法判断factory指向的对象是否还存在,所以不能直接调用。上面第四点说明了判断指针是否存在可以使用shared_ptr与weak_ptr来决定。
至于用哪个,得看Factory*是不是在这里必须存在,显然,这里只是清理Factory内部数据,如果Factory对象不在了,就不清理就好了。所以使用weak_ptr;
所以:
void deleteItem(Item* pItem, weak_ptr pFactory) {
share_ptr pFactoryShare = pFactory.lock();
if(pFactoryShare) {
pFactoryShare->deleteItem(pItem->getKey()); //pFactoryShare指向的对象一定存在。
}
delete pItem;
}
(5)如何将this变为shared_ptr或者weak_ptr,以方便在类成员函数内部调用shared_ptr为参数的函数?
上面第6点说明了,内部不可以直接使用shared_ptr
所以可以使用如下方式来实现:(思路固定。背下来就好)
class Factory : public std::enable_shared_from_this { //必须继承这个类;
//类内部需要使用shared_ptr的地方,使用shared_from_this()来代替。
//类内部需要使用weak_ptr的地方,使用std::weak_ptr(shared_from_this())就好了(就是使用shared_from_this()来生成了下weak_ptr())
};
通过相互使用weak_ptr,Item与Factory谁也不管谁,谁挂了也无所谓。
8. std::bind与std::function的简单理解。
使用起来比较简单,不再介绍,只介绍简单的理解。
std::function可以理解为可以统一函数格式,可以为函数重新命令。什么普通函数,类静态函数,类成员函数,经过function都变成了普通函数的格式,随便调用。(类成员函数的调用需要加上类对象指针)
std::bind不仅可以实现std::function函数的作用,还可以实现减少参数数量,添加默认参数等功能。
std::bind一个很常见的使用方式:为类静态函数和成员函数指定别名,简化类静态函数的调用方式。为成员函数提前加好类对象指针在第一个参数,后边再调用这个函数的时候,直接和调用类成员函数一样了。