线程安全的对象生命期管理
此章节开头的前两句话,把我点醒,原来思考功力可以这么深厚!如下:
-
第一句话: 编写线程安全的类不是难事, 用同步原语保护内部状态即可;
-
第二句话: 但是对象的生与死不能由对象自身拥有的mutex(互斥器) 来保护。
上述可以作为本章节的一个开篇词,值得每一位C++多线程开发者的回味!
1——析构遇到多线程
1.1——定义线程安全
1.2——如何保证对象构造时的线程安全?
1.3——Mutex真的可以保证对象析构时的安全?
1.4——Observer观察者模式的常规实现和线程安全性分析
1.5——智能指针简述
1.6——智能指针应用到Observer上
[1].析构遇到多线程
思考如下几个问题?
前提: 一个对象被多个线程同时能够观测到;
- 问题1:即将析构一个对象时, 如何知道是否有别的线程正在执行该对象的成员函数?
- 问题2:如何保证在执行成员函数期间,对象不会在另一个线程被析构?
- 问题3:在调用某个对象的成员函数之前, 如何得知这个对象还活着? 它的析构函数会不会碰巧执行到一半?
本章的意义: 利用shared_ptr解决上述模糊不清的资源竞争问题,减轻多线程编程的负担!
[1.1].定义线程安全
文献依据: Brian Goetz. Java Concurrency in Practice. Addison-Wesley, 2006
定义: 一个线程安全的类需要满足以下三个条件!
- 多个线程同时访问时, 其表现出正确的行为
- 无论操作系统如何调度这些线程, 无论这些线程的执行顺序如何交织, 其表现出正确的行为
- 调用端代码无须额外的同步或其他协调动作
附加: 依据这个定义, C++标准库里的大多数class都不是线程安全的, 包括std:: string、 std::vector、 std::map等
[1.2].如何保证对象构造时的线程安全?
一句话: 构造期间不要泄露this指针!
展开说:
- 不要在构造函数中注册任何回调
- 不要在构造函数中把this传给跨线程的对象
- 即便在构造函数的最后一行也不行
目的: 构造未完成,将this暴露在外,就类似衣服没穿,就出门了,必定发生难以预料的事情。
难易程度: 很容易保证。
[1.3].Mutex真的可以保证对象析构时的安全?
考虑如下的类Foo的代码:
主干: 一个析构函数、一个update函数
执行背景: 两个线程A和B,一个类Foo。A和B共同访问一个对象x,当A线程对x析构时,B线程对foo调用update
执行结果: 当线程A执行析构,互斥体已经被析构后;线程B又加锁。这种行为肯定是未定义的,其实这就跟访问已经释放的内存是一个道理!最好的情况也就是永远阻塞,很可能直接core dump!
结论: 作为数据成员的互斥锁并不能保护析构!
[1.4].Observer观察者模式的常规实现和线程安全性分析
代码如下:
简单讲解: Observer是观察者,Observable是可被观察的物体。可被观察的物体通过register_成员函数,能够添加多个观察者。这样物体每次变化的时候,只需要调用nofifyObservers即可对所有观察者进行通知,实现一对多的消息传递!
多线程场景分析: Observable类的nofifyObservers()在遍历观察者的时候,17行这里,它如何得知x是否已经消亡了呢?同理,Observer在析构中,32行,它如何得知subject_是否存活呢?这些都是线程安全的观察者模式的实现难点!
关键点猜测: 似乎线程安全的关键点在于,如何通过指针能够判断对象是否还活着?类似这种的功能,仔细想象这不就是类似于代理的功能么,原生指针肯定没办法,所以需要一个wrapper类,也就是智能指针,它包括着原始指针!
[1.5].智能指针简述
智能指针解决的最典型问题:
1、空悬指针
如上图,加入P1和P2的原始指针都指向Object,如果P1将指针置空,但是P2并不知道,再使用就出错了!
如何解决的?
思路一: 引入间接访问层,让P1和P2指向的对象永久有效,这个对象持有Object指针,如下图所示:
当Object被销毁,proxy对象仍然存在,其内存指针值为0,可以通过proxy的接口进行判断对象是否存活!如下:
存在的问题: 线程安全的释放Object并不容易,竞争仍然存在;比如:当P2去看proxy内容不为空,数据存在。这时去调用Object的成员函数,但是在调用过程中对象被P1销毁了,这时候又出现问题!
思路二: 为了安全释放proxy,引入引用计数,Object对象的生命周期完全由proxy掌控,如下图:
(1)一开始,两个引用,计数为2
(2)sp1析构了,计数-1
(3)sp2析构了,计数-1,这时为0,Object自动由proxy释放内存
shared_ptr简述: 它被C++11标准库引入,是一个类模板,利用引入计数进行自动化的资源管理,引用计数为0,对象销毁,强引用,控制对象生命周期!
weak_ptr简述: 同理,也是C++11引入,但是它不直接增加对象引用计数,是从shared_ptr偷来的资源使用,是弱引用,不控制对象生命周期,通过线程安全的接口lock()进行提升为shared_ptr,从而判断对象是否已经释放!
shared_ptr的线程安全性: 本身不是100%线程安全,它的引用计数是安全且无所,但对象的读写不是,因此不是线程安全的。如何评价呢?三句话!如下:
- 一个shared_ptr对象实体可被多个线程同时读取
- 两个shared_ptr对象实体可以被两个线程同时写入, “析构”算写操作;
- 如果要从多个线程读写同一个shared_ptr对象, 那么需要加锁。
例子如下:
分析: 单纯的针对同一个智能指针对象进行读写的时候,需要加锁。然后创建局部智能指针对象后,这样针对局部的智能指针的读写操作都不需要加锁!
智能指针作为函数参数——最常见使用方式: 使用const reference方式传递智能指针,只需要在最外层由一个local shared_ptr实体,然后调用函数都通过这个实体的const引用即可!如下图:
综上: shared_ptr的线程安全级别和STL容器一致,并不是线程安全的!多线程场景下需要注意再注意,同时在作为参数传递时,也需要小心再小心!
[1.6].智能指针应用到Observer上
针对Observable的改造如下图所示:
评价: 这里虽然解决了Observer模拟的线程安全问题,但是仍有许多问题,疑点重重。如下:
疑点:
- 侵入性。强制要求Observer必须以shared_ptr来管理;
- 不是完全线程安全。Observer的析构函数会调用subject_->unregister(this), 万一subject_已经不复存在了呢? 为了解决它, 又要求Observable本身是用shared_ptr管理的, 并且subject_多半是个weak_ptr;
- 锁争用。Observable的三个成员函数都用了互斥器来同步,会造成register_()和unregister()等待notifyObservers(), 而
后者的执行时间是无上限的; - 死锁。万一L62的update()虚函数中调用了(un)register呢? 如果mutex_是不可重入的, 那么会死锁!
结尾: 我是航行的小土豆,喜欢我的程序猿朋友们,欢迎点赞+关注哦!希望大家多多支持我哦!有相关不懂问题,可以留言一起探讨哦!