文章目录

  • 索引
  • 一、线程安全的对象生命期管理
  • 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++多线程系统编程精要

Java Linux服务器多线程配置 linux多线程服务器端编程_服务器开发

一、线程安全的对象生命期管理

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不是办法】

Java Linux服务器多线程配置 linux多线程服务器端编程_Linux_02


【mutex不能保护析构】:数据成员mutex只能同步于本class其他成员的读写;由于mutex生命周期最多=对象的,故不能保护析构

  • 一个函数锁住相同类型的多个对象,始终先加锁地址较小的mutex

1.3 线程安全的observer多难

【判断一个指针是否存活?】:若销毁,则无法访问无法获取对象状态,也可能在源地址上创建新的对象;故,无法判断

【对象的关系】

  • 组合/复合:不会出现线程安全;
  • 关系/联系:一个对象a用到了另一个对象b,调用了后者的成员函数,a持有b指针/引用,但其生命周期不受a单独控制;
  • 聚合:b是动态创建并在程序中可能提前释放

【解决方法】:

  • 只创建不销毁,程序使用对象池暂存,用完将其放回供下次使用

1.4 原始指针有什么不好的地方

当暴露给其他线程这是不好的,一般使用智能指针,但智能指针在以下情况使用可能会引起循环引用

Java Linux服务器多线程配置 linux多线程服务器端编程_Linux_03


【空悬指针】:当两个指针指向同一个对象不同线程将其中一个对象销毁,而另外一个就成了空悬指针;

  • 【方法一】:中间引入间接层(proxy指针),让两个指针指向的对象永久有效;(即二级指针);
    当对象销毁后,proxy继续存在,但只变为0;【但该如何释放proxy指针】
  • 【方法二】:引入引用计数,析构减1,创建加1,为0时销毁;
  • 【方法三】:使用智能指针;

1.5 shared_ptr或weak_ptr

shared_ptr或weak_ptr参考 【shared_ptr】:当对象最后一个时,进行析构reset会被销毁;

1.6 系统地避免指针错误

  • 缓冲区溢出:需记住缓冲区的长度,并通过成员函数来修改长度,不能使用裸指针
  • 空悬指针/野指针:用shared_ptrweak_ptr
  • 重复释放:用scoped_ptr/unique_ptr,只在都析构的时候释放一次
  • 内存泄漏:用scoped_ptr/unique_ptr,对象析构的时候自动释放内存;
  • 不配对new/delete:替换成vector或scoped_array;
  • 内存碎片:后续补充;

1.7 应用到Observer上

借助weak_ptr探查对象的生死,可以用来解决竞态条件;weak_ptr对象被释放后则为空,可以通过判断该对象是否还存在从而进行其他操作;

Java Linux服务器多线程配置 linux多线程服务器端编程_Java Linux服务器多线程配置_04


还会有其他疑点

【不灵活】:必须使用智能指针来管理;

【锁争用】:当Observer三个成员函数都使用互斥锁来同步,会造成register_unregister等待同步调用update的notifyObserver执行时间无上限;

【死锁】:若L62中的update调用register_或unregister,若mutex_是不可重入,则会死锁;若可重入,则会导致迭代器失效,由于vector在遍历期间被修改;

1.8 再论shared_ptr线程安全

本身是安全,且无锁,原子操作;但对象的读写成员不能原子化

  • 一个shared_ptr对象实体可被多个线程同时读取;
  • 两个shared_ptr对象实体可被两个线程同时写入,析构为写操作
  • 若多个线程同一个shared_ptr对象,则需加锁;(但不必用读写锁,可以使用互斥锁,不会阻塞并发读);

Java Linux服务器多线程配置 linux多线程服务器端编程_开发语言_05


另外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并定制一个析构功能,可使用函数指针仿函数

Java Linux服务器多线程配置 linux多线程服务器端编程_Linux_06

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中,即生命周期不会被延长

Java Linux服务器多线程配置 linux多线程服务器端编程_c++_07

1.11 替代方案

不使用智能指针,而确保线程安全的对象回调与析构:

  • 只创建不销毁;
  • 自编写引用计数智能指针;
  • unique_ptr;

1.12 Observer的不足

Observer设计模式

Observer带来的强耦合,可能需要使用到多继承,再C++中,可使用functionbind来解决;

【可使用变长模板替代】:

Java Linux服务器多线程配置 linux多线程服务器端编程_Java Linux服务器多线程配置_08


【线程安全版】:

https://github.com/chenshuo/recipes/blob/master/thread/SignalSlot.h

参考:《Linux多线程服务端编程》陈硕