文章目录
- 索引
- 一、线程安全的对象生命期管理
- 1.1 析构函数遇多线程
- 1.2 对象销毁
- 1.3 线程安全的observer多难
- 1.4 原始指针有什么不好的地方
- 1.5 shared_ptr或weak_ptr
- 1.6 系统地避免指针错误
- 1.7 应用到Observer上
- 1.8 再论shared_ptr线程安全
- 1.9 shared_ptr技术与陷阱
- 1.10 对象池
- 1.10.1 enable_shared_from_this
- 1.10.2 弱回调
- 1.11 替代方案
- 1.12 Observer的不足
索引
【Linux多线程服务端编程】| 【01】线程安全的对象生命期管理笔记【Linux多线程服务端编程】| 【02】线程同步精要【Linux多线程服务端编程】| 【03】多线程服务器的适用场合和常用编程模型【Linux多线程服务端编程】| 【04】C++多线程系统编程精要
一、线程安全的对象生命期管理
1.1 析构函数遇多线程
多线程下对象的销毁时机不确定
;
- 无法确保是否有
其他线程在执行该对象的成员函数
; - 如何确保成员函数
执行期间
,对象不被其他线程析构
; -
调用成员函数前
,确保该对象还存活
;
【线程安全满足】:
- 多线程同时
访问
时,行为正确,无论线程如何交织
; -
调用端
无需额外的同步动作
;
【对象构造
要线程安全】:
- 不要再构造函数中注册任何
回调
; - 不要再构造中将
this
传给跨线程
对象;
【二段式构造】:构造 + init() —— 于多线程下
class Foo : public Observer{
public:
Foo(Observer* s) {
s->register(this);
}
}; // 错误做法
// 正确做法
class Foo : public Observer{
public:
Foo();
void observer(Observer* s) {
s->register(this);
}
};
Foo* foo = new Foo;
Observer* s = getSub();
foo->observer(s);
1.2 对象销毁
【mutex不是办法】
【mutex不能保护析构】:数据成员mutex只能同步
于本class其他成员的读写
;由于mutex生命周期最多=对象的
,故不能保护析构
;
- 一个函数锁住相同类型的多个对象,
始终先加锁地址较小的mutex
;
1.3 线程安全的observer多难
【判断一个指针是否存活?】:若销毁
,则无法访问
,无法获取对象状态
,也可能在源地址上创建新的对象;故,无法判断
;
【对象的关系】
- 组合/复合:不会出现线程安全;
- 关系/联系:一个对象a用到了另一个对象b,调用了
后者
的成员函数,a持有b指针/引用
,但其生命周期不受a单独
控制; - 聚合:b是动态创建并在程序中可能
提前释放
;
【解决方法】:
-
只创建不销毁
,程序使用对象池暂存
,用完将其放回供下次使用
;
1.4 原始指针有什么不好的地方
当暴露给其他线程这是不好的,一般使用智能指针
,但智能指针在以下情况使用可能会引起循环引用
;
【空悬指针】:当两个指针指向同一个对象
,不同线程
将其中一个对象销毁
,而另外一个就成了空悬指针;
- 【方法一】:中间引入
间接层
(proxy指针),让两个指针指向的对象永久有效;(即二级指针);
当对象销毁后,proxy继续存在
,但只变为0;【但该如何释放proxy指针】 - 【方法二】:引入引用计数,析构减1,创建加1,为0时销毁;
- 【方法三】:使用智能指针;
1.5 shared_ptr或weak_ptr
shared_ptr或weak_ptr参考 【shared_ptr】:当对象最后一个时,进行析构
或reset
会被销毁;
1.6 系统地避免指针错误
- 缓冲区溢出:需记住缓冲区的
长度
,并通过成员函数来修改长度,不能使用裸指针
; - 空悬指针/野指针:用
shared_ptr
或weak_ptr
; - 重复释放:用
scoped_ptr
/unique_ptr
,只在都析构的时候释放一次
; - 内存泄漏:用
scoped_ptr
/unique_ptr
,对象析构的时候自动释放
内存; - 不配对new/delete:替换成
vector
或scoped_array; - 内存碎片:后续补充;
1.7 应用到Observer上
借助weak_ptr探查对象的生死
,可以用来解决竞态条件
;weak_ptr对象被释放后则为空
,可以通过判断该对象是否还存在从而进行其他操作;
还会有其他疑点:
【不灵活】:必须使用智能指针
来管理;
【锁争用】:当Observer三个成员函数都使用互斥锁
来同步,会造成register_
和unregister
等待同步
调用update的notifyObserver执行时间无上限;
【死锁】:若L62中的update调用register_或unregister,若mutex_是不可重入
,则会死锁
;若可重入
,则会导致迭代器失效
,由于vector在遍历期间被修改;
1.8 再论shared_ptr线程安全
本身是安全
,且无锁,原子操作
;但对象的读写
成员不能原子化
;
- 一个shared_ptr对象实体可被多个线程同时读取;
- 两个shared_ptr对象实体可被两个线程同时写入,
析构为写操作
; - 若多个线程
写
同一个shared_ptr对象,则需加锁
;(但不必用读写锁,可以使用互斥锁,不会阻塞并发读
);
另外localPtr = globalPtr
==swap(),保证对象的销毁推迟到临界区
外;
而write函数内,globalPtr = newPtr,原globalPtr函数可能在临界区销毁;【该如何移出临界区?】
- 使用临时保存该指针,在临界区外进行reset;
1.9 shared_ptr技术与陷阱
【注意引用计数】:
- 如上述代码Observers_如何将类型该为
vector<shared_ptr<Observer\> >
,如果不手动调用unregister对象将不会析构; - 还有可能出现在
bind
绑定函数参数
上,由于会产生实参拷贝
,可能导致引用计数的增加
;
- shared_ptr
拷贝开销
比拷贝原始指针高
,可以使用const reference
;
【智能指针的析构动作在创建是被捕获】即:
- 虚析构
不再必需
; - shared_ptr<void>可持有任何对象,
能安全释放
; - shared_ptr可安全跨模块边界???;
-
二进制兼容性
,若对象大小变了,则旧客户代码仍可使用新的动态库无需编译(不能有inline); - 析构动作
可定制
; - 析构行为可以是
函数指针
,仿函数
等; - 析构所在线程,对象的
析构时同步
的,当最后一个shared_ptr离开作用域时,会同时在同一个线程析构(析构比较耗时
,可能会拖延关键线程),我们可以使用单独的线程
来做析构;
std::shared_ptr<A> ptr(new A, [](A* a){
cout << "xxxx" << endl;
delete a;
});
1.10 对象池
使用shared_ptr作为对象池
中的元素,很可能导致对象不会被销毁
,则使用weak_ptr;
std::map<string, std::shared_ptr<Stock> > stocks_;
==> 但可以通过使用weak_ptr来解决
std::map<string, std::weak_ptr<Stock> > stocks_;
但会触发新问题,stocks_的大小只增不减
,造成内存泄漏
;
【解决方法】:使用shared_ptr并定制一个析构
功能,可使用函数指针
或仿函数
;
1.10.1 enable_shared_from_this
【该方法能解决上述的线程安全问题
:获取一个指向当前对象的shared_ptr<StockFactory\>
对象】
enable_shared_from_this为基类
模板,可以让类的this指针变为shread_ptr;
- 继承该基类,必须是一个
堆对象
,由shared_ptr来管理; - shared_from_this不能再
构造
中调用,由于对象还在构造
,还没有交给shared_ptr接管; -
延长了生命周期
;
通过继承该基类,来确保上述的问题,即在bind中,传入的this还存活着;
1.10.2 弱回调
解释:弱该对象还活着
,即调用它的成员函数,否则忽略之,利用weak_ptr;
- 可以把weak_ptr绑定到function中,即
生命周期不会被延长
;
1.11 替代方案
不使用智能指针,而确保线程安全的对象回调与析构:
- 只创建不销毁;
- 自编写引用计数智能指针;
- unique_ptr;
1.12 Observer的不足
Observer带来的强耦合
,可能需要使用到多继承,再C++中,可使用function
和bind
来解决;
【可使用变长模板替代】:
【线程安全版】:
https://github.com/chenshuo/recipes/blob/master/thread/SignalSlot.h
参考:《Linux多线程服务端编程》陈硕