C++中实现线程平安的对象创立、回调与析构
- 写出线程平安的类并不难,应用同步原语爱护外部状态即可
- STL大多类都不是线程平安的,须要在内部加锁保障多个线程同时拜访
平安的对象创立
惟一的要求就是不要在结构期间泄露this指针,即
- 不要在构造函数中注册任何回调
- 不要把this指针传给跨线程的对象
- 即便在最初一行也不能够,因为这个类可能是基类,它的构造函数最初一行不等于结构实现
起因:在执行构造函数期间对象没有实现初始化,如果this指针被写了泄露给了其余对象,那么别的线程可能会拜访这个半成品
平安的对象销毁
对象析构函数的竞态条件
1.析构某个对象时,如何得悉是否有他人在用它?
2.析构某个对象时,它是否可能正在被另一个线程析构?
3.调用某个对象的成员函数时,如何保障它仍旧活着?析构函数会不会碰巧执行到一半?
对象析构函数的设计难点
1.mutex不是方法,因为锁只是用来爱护对象外部数据的,不能来爱护对象自身,不能解决任何问题
2.面向对象中宽泛应用的三种对象关系,都依赖于对象晓得“其余对象是否还活着”的能力
a. composition
b. aggregation
c. association
其中,composition在多线程中不会有什么麻烦,因为对象x
的生命周期由其owner
惟一管制,但后两者,对象的分割是借由指针或援用实现的,因而可能会呈现下面提到的竞态条件。
解决办法 :shared_ptr / weak_ptr
即在对象间中引入一层中间层,不再应用裸指针,而是智能指针,作为一个辅助的管理者(援用计数)。
shared_ptr
管制对象的生命周期,是强援用,只有有一个指向x
对象的shared_ptr
存在,x
就不会析构,当援用计数降为0
时或reset()
时,x
保障会被销毁。weak_ptr
不管制对象生命周期,但晓得对象是否或者,如果对象没死,那么能够晋升为无效的shared_ptr
,从而援用对象,并保障在这个期间对象不析构,如果对象死了,那么会返回空的shared_ptr
。晋升行为是线程平安的。shared_ptr
和weak_ptr
的计数是原子操作,性能不俗
然而
- ❗
shared_ptr
和weak_ptr
自身的线程安全级别与std::string
和STL的容器一样,见上面的探讨
对于shared_ptr的探讨
shared_ptr
并不线程平安:能够多个线程同时读,不能多个线程同时写(析构算写)shared_ptr
的线程平安和它指向对象的线程平安是两回事-
访问共享的
shared_ptr
,须要加上互斥锁void read(){ shared_ptr<Foo> localPtr; { MutexLockGuard lock(mutex); localPtr = globalPtr; // read globalPtr } // use localPtr since here,读写localPtr无需加锁,因为是栈上对象,只有以后线程可见 do_it(localPtr); // 这里函数的参数应是reference to const,防止复制 }
weak_ptr
和shared_ptr
用错,可能会导致一些对象永远不被析构,通常做法是owner领有指向child的shared_ptr
,child持有owner的weak_ptr
- 拷贝开销:因为拷贝会创立一份正本,须要批改援用计数,开销较大,但须要拷贝的中央不多,通常
shared_ptr
作为参数时都是const reference
。
RAII 资源获取即初始化
- C++区别于其余编程语言的最重要的个性
- 每一个明确的资源配置动作(如new)都应该在繁多语句中执行,并在该寄居中立即将配置取得的资源交给handle对象(如
shared_ptr
),程序中个别不呈现delete
线程平安的对象池
实现一个StcokFactory
, 依据key返回Stock对象
版本1: 利用weak_ptr
和shared_ptr
治理
class StockFactory : boost::noncopyable
{
public:
boost::shared_ptr<Stock> get(const string& key)
{
boost::shared_ptr<Stock> pStock;
muduo::MutexLockGuard lock(mutex_);
boost::weak_ptr<Stock>& wkStock = stocks_[key]; // 如果这里是shared_ptr,则对象永远不会被析构
pStock = wkStock.lock();
if (!pStock)
{
pStock.reset(new Stock(key));
wkStock = pStock; // wkStock is a reference
}
return pStock;
}
private:
mutable muduo::MutexLock mutex_;
std::map<string, boost::weak_ptr<Stock> > stocks_;
};
问题: 存在轻微的内存透露,map
中的key:weak_ptr会始终存在,在key无限时没什么问题,在key范畴很大时会造成内存透露
版本2: 利用shared_ptr
的定制析构性能,在析构对象的同时析构
class StockFactory : boost::noncopyable
{
public:
boost::shared_ptr<Stock> get(const string& key)
{
boost::shared_ptr<Stock> pStock;
muduo::MutexLockGuard lock(mutex_);
boost::weak_ptr<Stock>& wkStock = stocks_[key];
pStock = wkStock.lock();
if (!pStock)
{
pStock.reset(new Stock(key),
boost::bind(&StockFactory::deleteStock, this, _1)); // 定制析构性能,绑定了一个对象的成员函数
wkStock = pStock;
}
return pStock;
}
private:
void deleteStock(Stock* stock)
{
printf("deleteStock[%p]\n", stock);
if (stock)
{
muduo::MutexLockGuard lock(mutex_);
stocks_.erase(stock->key()); // 如果stocks_先于stock死亡,这里会core dump
}
delete stock; // 因为本人定制析构性能,须要手动写delete
}
mutable muduo::MutexLock mutex_;
std::map<string, boost::weak_ptr<Stock> > stocks_;
};
问题: 如果stocks_
先于stock
死亡,stock
析构,回调stocks_
的erase
函数时会报错,这个问题的实质和下面探讨的问题是一样的,即对象的this
指针是不足够判断对象生命周期的,下面的办法里,改用weak_ptr
+ shared_ptr
解决了问题,这里也一样
版本3: 利用 enable_shared_from_this
应用shared_ptr
代替this
指针
class StockFactory : public boost::enable_shared_from_this<StockFactory>,
boost::noncopyable
{
public:
boost::shared_ptr<Stock> get(const string& key)
{
boost::shared_ptr<Stock> pStock;
muduo::MutexLockGuard lock(mutex_);
boost::weak_ptr<Stock>& wkStock = stocks_[key];
pStock = wkStock.lock();
if (!pStock)
{
pStock.reset(new Stock(key),
boost::bind(&StockFactory::deleteStock,
shared_from_this(),
_1));
wkStock = pStock;
}
return pStock;
}
private:
void deleteStock(Stock* stock)
{
printf("deleteStock[%p]\n", stock);
if (stock)
{
muduo::MutexLockGuard lock(mutex_);
stocks_.erase(stock->key()); // This is wrong, see removeStock below for correct implementation.
}
delete stock;
}
mutable muduo::MutexLock mutex_;
std::map<string, boost::weak_ptr<Stock> > stocks_;
};
问题: 因为stock
注册了StockFactory
的shared_ptr
(bind的时候会产生指针的复制),导StockFactory
会等到所有stock
都析构之后才析构,意外地缩短了生命。
版本4: 利用enable_shared_from_this
应用weak_ptr
代替this
指针,实现弱回调 : “如果对象还活着,就调用它的成员函数,否则疏忽之”
class StockFactory : public boost::enable_shared_from_this<StockFactory>,
boost::noncopyable
{
public:
boost::shared_ptr<Stock> get(const string& key)
{
boost::shared_ptr<Stock> pStock;
muduo::MutexLockGuard lock(mutex_);
boost::weak_ptr<Stock>& wkStock = stocks_[key];
pStock = wkStock.lock();
if (!pStock)
{
pStock.reset(new Stock(key),
boost::bind(&StockFactory::weakDeleteCallback,
boost::weak_ptr<StockFactory>(shared_from_this()),
// 必须有这步转型,才不会缩短StockFactory的生命周期
// boost:bind拷贝的是实参类型不是形参类型
_1));
wkStock = pStock;
}
return pStock;
}
private:
static void weakDeleteCallback(const boost::weak_ptr<StockFactory>& wkFactory,
Stock* stock)
{
printf("weakDeleteStock[%p]\n", stock);
boost::shared_ptr<StockFactory> factory(wkFactory.lock());
if (factory)
{
factory->removeStock(stock);
}
else
{
printf("factory died.\n");
}
delete stock;
}
void removeStock(Stock* stock)
{
if (stock)
{
muduo::MutexLockGuard lock(mutex_);
auto it = stocks_.find(stock->key());
if (it != stocks_.end() && it->second.expired())
{
stocks_.erase(stock->key());
}
}
}
private:
mutable muduo::MutexLock mutex_;
std::map<string, boost::weak_ptr<Stock> > stocks_;
};
这下无论Stock还是StockFactory谁先析构都不会影响程序失常运行,利用智能指针解决了两个对象互相援用的问题。
当然,通常Factory对象是一个singleton,并且失常运行期间不会销毁,这里只是为了展现弱回调计数。
多线程编程的Tips
1.包装底层的同步原语,借助对象的结构和析构加锁和解锁
int getvalue() const{
MutexLockGuard lock(mutex_); // 对象的结构即加锁
return value_; // 读取数据
} // 作用域完结,Guard主动析构,解锁
2.如果一个函数要所著雷同类型的两个对象,那么为了保障始终按雷同的程序加锁,能够比拟mutex对象的地址始终加锁地址较小的那个,比方:
void swap(counter& a, counter& b){
MutexLockGuard aLock(a.mutex_);
MutexLockGuard bLock(b.mutex_);
// 替换a,b的数据
}
如果线程A执行swap(a,b)
而线程B执行swap(b,a)
将会产生死锁。
3.尽管本章讲如何平安应用、析构跨线程的对象,然而尽量不要用跨线程的对象,用流水线,生产者消费者,工作队列等有法则的机制,最低限度的共享数据,是比拟好的多线程编程办法
发表回复