<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理

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

此章节开头的前两句话,把我点醒,原来思考功力可以这么深厚!如下:

  • 第一句话: 编写线程安全的类不是难事, 用同步原语保护内部状态即可;

  • 第二句话: 但是对象的生与死不能由对象自身拥有的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函数

<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第1张图片

执行背景: 两个线程A和B,一个类Foo。A和B共同访问一个对象x,当A线程对x析构时,B线程对foo调用update

<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第2张图片

执行结果: 当线程A执行析构,互斥体已经被析构后;线程B又加锁。这种行为肯定是未定义的,其实这就跟访问已经释放的内存是一个道理!最好的情况也就是永远阻塞,很可能直接core dump!

结论: 作为数据成员的互斥锁并不能保护析构!

[1.4].Observer观察者模式的常规实现和线程安全性分析

代码如下:

<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第3张图片
<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第4张图片

简单讲解: Observer是观察者,Observable是可被观察的物体。可被观察的物体通过register_成员函数,能够添加多个观察者。这样物体每次变化的时候,只需要调用nofifyObservers即可对所有观察者进行通知,实现一对多的消息传递!

多线程场景分析: Observable类的nofifyObservers()在遍历观察者的时候,17行这里,它如何得知x是否已经消亡了呢?同理,Observer在析构中,32行,它如何得知subject_是否存活呢?这些都是线程安全的观察者模式的实现难点!

关键点猜测: 似乎线程安全的关键点在于,如何通过指针能够判断对象是否还活着?类似这种的功能,仔细想象这不就是类似于代理的功能么,原生指针肯定没办法,所以需要一个wrapper类,也就是智能指针,它包括着原始指针!

[1.5].智能指针简述

智能指针解决的最典型问题:

1、空悬指针
<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第5张图片
如上图,加入P1和P2的原始指针都指向Object,如果P1将指针置空,但是P2并不知道,再使用就出错了!

如何解决的?

思路一: 引入间接访问层,让P1和P2指向的对象永久有效,这个对象持有Object指针,如下图所示:
<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第6张图片
当Object被销毁,proxy对象仍然存在,其内存指针值为0,可以通过proxy的接口进行判断对象是否存活!如下:
<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第7张图片

存在的问题: 线程安全的释放Object并不容易,竞争仍然存在;比如:当P2去看proxy内容不为空,数据存在。这时去调用Object的成员函数,但是在调用过程中对象被P1销毁了,这时候又出现问题!

思路二: 为了安全释放proxy,引入引用计数,Object对象的生命周期完全由proxy掌控,如下图:

(1)一开始,两个引用,计数为2
<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第8张图片
(2)sp1析构了,计数-1
<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第9张图片
(3)sp2析构了,计数-1,这时为0,Object自动由proxy释放内存
<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第10张图片

shared_ptr简述: 它被C++11标准库引入,是一个类模板,利用引入计数进行自动化的资源管理,引用计数为0,对象销毁,强引用,控制对象生命周期!

weak_ptr简述: 同理,也是C++11引入,但是它不直接增加对象引用计数,是从shared_ptr偷来的资源使用,是弱引用,不控制对象生命周期,通过线程安全的接口lock()进行提升为shared_ptr,从而判断对象是否已经释放!

shared_ptr的线程安全性: 本身不是100%线程安全,它的引用计数是安全且无所,但对象的读写不是,因此不是线程安全的。如何评价呢?三句话!如下:

  • 一个shared_ptr对象实体可被多个线程同时读取
  • 两个shared_ptr对象实体可以被两个线程同时写入, “析构”算写操作;
  • 如果要从多个线程读写同一个shared_ptr对象, 那么需要加锁。

例子如下:
<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第11张图片
分析: 单纯的针对同一个智能指针对象进行读写的时候,需要加锁。然后创建局部智能指针对象后,这样针对局部的智能指针的读写操作都不需要加锁!

智能指针作为函数参数——最常见使用方式: 使用const reference方式传递智能指针,只需要在最外层由一个local shared_ptr实体,然后调用函数都通过这个实体的const引用即可!如下图:
<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第12张图片<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第13张图片

综上: shared_ptr的线程安全级别和STL容器一致,并不是线程安全的!多线程场景下需要注意再注意,同时在作为参数传递时,也需要小心再小心!

[1.6].智能指针应用到Observer上

针对Observable的改造如下图所示:

<<Linux多线程服务端编程>>学习之栏1————线程安全的对象生命期管理_第14张图片
评价: 这里虽然解决了Observer模拟的线程安全问题,但是仍有许多问题,疑点重重。如下:

疑点:

  • 侵入性。强制要求Observer必须以shared_ptr来管理;
  • 不是完全线程安全。Observer的析构函数会调用subject_->unregister(this), 万一subject_已经不复存在了呢? 为了解决它, 又要求Observable本身是用shared_ptr管理的, 并且subject_多半是个weak_ptr;
  • 锁争用。Observable的三个成员函数都用了互斥器来同步,会造成register_()和unregister()等待notifyObservers(), 而
    后者的执行时间是无上限的;
  • 死锁。万一L62的update()虚函数中调用了(un)register呢? 如果mutex_是不可重入的, 那么会死锁!

结尾: 我是航行的小土豆,喜欢我的程序猿朋友们,欢迎点赞+关注哦!希望大家多多支持我哦!有相关不懂问题,可以留言一起探讨哦!

如有引用或转载记得标注哦!

你可能感兴趣的:(c++,linux,服务器,网络,系统安全)