C++ 全栈知识体系C++ 全栈知识体系
✿导航
  • 基础
  • 函数
  • 知识点
  • IO框架
  • 新版本特性
  • 数据库原理
  • SQL语言
  • SQL - MySQL
  • NoSQL - Redis
  • NoSQL - ElasticSearch
  • 算法基础
  • 常见算法
  • 领域算法
  • 分布式算法
  • 数据结构与算法
  • 计算机网络
  • 操作系统
  • 计算机组成
  • 开发
  • 测试
  • 架构基础
  • 分布式系统
  • 微服务
  • 中间件
  • 概念
  • 理论
  • 架构设计原则
  • 设计模式
  • 协议
  • 技术选型
  • 编码规范
  • 流水线构建 - CI/CD
  • 知识点 - Linux
  • 网站 - Nginx
  • 容器化 - Docker
  • 容器编排 - Kubernetes
  • 服务网格 - Service Mesh Istio
  • 常用快捷键 - Shortcut
  • 工具使用 - Tools
  • 开源项目
  • 学习项目
  • 个人项目
  • 项目开发
  • 项目Idea
  • 并发
  • 部署
  • 分布式
  • 知识
  • 问题
  • 编程语言与技术
  • 系统与架构
  • 软件开发实践
  • 数据处理与应用设计
  • 个人
  • 产品
  • 团队
  • 知识体系
  • Vue
关于
✿导航
  • 基础
  • 函数
  • 知识点
  • IO框架
  • 新版本特性
  • 数据库原理
  • SQL语言
  • SQL - MySQL
  • NoSQL - Redis
  • NoSQL - ElasticSearch
  • 算法基础
  • 常见算法
  • 领域算法
  • 分布式算法
  • 数据结构与算法
  • 计算机网络
  • 操作系统
  • 计算机组成
  • 开发
  • 测试
  • 架构基础
  • 分布式系统
  • 微服务
  • 中间件
  • 概念
  • 理论
  • 架构设计原则
  • 设计模式
  • 协议
  • 技术选型
  • 编码规范
  • 流水线构建 - CI/CD
  • 知识点 - Linux
  • 网站 - Nginx
  • 容器化 - Docker
  • 容器编排 - Kubernetes
  • 服务网格 - Service Mesh Istio
  • 常用快捷键 - Shortcut
  • 工具使用 - Tools
  • 开源项目
  • 学习项目
  • 个人项目
  • 项目开发
  • 项目Idea
  • 并发
  • 部署
  • 分布式
  • 知识
  • 问题
  • 编程语言与技术
  • 系统与架构
  • 软件开发实践
  • 数据处理与应用设计
  • 个人
  • 产品
  • 团队
  • 知识体系
  • Vue
关于
  • 编程语言与技术

    • Effective C++: 改善程序与设计的55个具体做法

      • 第2章 - 构造/析构/赋值运算(一)
      • 第2章 - 构造/析构/赋值运算(二)
      • 第2章 - 构造/析构/赋值运算(三)
      • 第3章 - 资源管理
      • 第4章 - 设计与声明(一)
      • 第4章 - 设计与声明(二)
      • 第5章 - 实现(一)
      • 第5章 - 实现(二)
      • 第6章 - 继承与面向对象设计
      • 第7章 - 模板与泛型编程
    • 深度探索C++对象模型

      • 第1章 - 关于对象
      • 第2章 - 构造函数语意学
      • 第3章 - Data 语意学
    • STL源码剖析

      • 第1章 - STL概论和版本简介
      • 第2章 - 空间配置器
      • 第3章 - 迭代器(iterators)概念与traits编程技法(一)
      • 第3章 - 迭代器(iterators)概念与traits编程技法(二)
      • 第4章 - 序列式容器 vector
      • 第4章 - 序列式容器 list
      • 第4章 - 序列式容器 deque
      • 第4章 - 序列式容器 stack和queue
      • 第4章 - 序列式容器 heap
      • 第4章 - 序列式容器 priority_queue
      • 第4章 - 序列式容器 slist
      • 第5章 - 关联式容器 RB-tree
      • 第5章 - 关联式容器 set和map
      • 第5章 - 关联式容器 hashtable
      • 第6章 - 算法
      • 第6章 - 算法之set
      • 第7章 - 仿函数
      • 第8章 - 配接器
  • 系统与架构

    • 深入理解计算机系统

      • 第1章 - 计算机系统漫游
      • 第2章 - 信息的表示和处理
      • 第3章 - 程序的机器级表示
      • 第5章 - 优化程序性能
      • 第6章 - 存储器层次结构
      • 第7章 - 链接
      • 第8章 - 异常控制流
      • 第9章 - 虚拟内存
      • 第10章 - 系统级I/O
      • 第11章 - 网络编程
      • 第12章 - 并发编程
    • 大型网站技术架构——核心原理与案例分析

      • 第1章 - 大型网站架构演化
      • 第2章 - 大型网站架构模式
      • 第3章 - 大型网站核心架构要素
      • 第4章 - 瞬时响应:网站的高性能架构
      • 第5章 - 万无一失:网站的高可用架构
      • 第6章 - 永无止境:网站的伸缩性架构
      • 第7章 - 随需应变:网站的可扩展架构
      • 第8章 - 固若金汤:网站的安全架构
    • 从零开始学架构

      • 架构基础
      • 架构设计原则
      • 高性能架构
      • 高可用架构
    • 程序员的自我修养————链接、装载与库

      • 第1章 - 简介
      • 第2章 - 静态链路
      • 第3章 - 目标文件里有什么
      • 第4章 - 静态链接
      • 第7章 - 动态链接
      • 第8章 - 共享库版本
      • 第10章 - 内存
      • 第11章 - 运行库
      • 第12章 - 系统调用与API
      • 第13章 - 运行库实现
  • 软件开发实践

    • 重构改善既有代码的设计

      • 第1章 - 重构,第一个示例
      • 第2章 - 重构的原则
      • 第3章 - 代码的坏味道
      • 第5章 - 重构列表
      • 第6章 - 重新组织函数
      • 第7章 - 在对象之间搬移特性
      • 第8章 - 重新组织数据
      • 第9章 - 简化条件表达式
      • 第10章 - 简化函数调用
      • 第11章 - 处理概括关系
      • 第12章 - 设计之大型重构
    • 代码大全2

      • 第1章 - 欢迎进入软件构建的世界
      • 第2章 - 用隐喻来更充分地理解软件开发
      • 第3章 - 三思而后行: 前期准备
      • 第4章 - 关键的构建决策
      • 第5章 - 软件构建中的设计
    • Linux多线程服务端编程——使用muduo C++ 网络库

      • Buffer类的设计
      • 设计与实现
      • 定时器与TimerQueue
      • Protobuf网络传输和Protobuf编解码器与消息分发器
      • EventLoop类剖析
      • EventLoopThread和EventLoopThreadPool剖析
      • TCP网络库和核心类
      • Connector剖析
      • TcpClient剖析
      • 学习总结
      • timing wheel
      • 消息广播服务
      • 线程安全的对象生命期管理
  • 数据处理与应用设计

    • 数据密集型应用系统设计

      • 第1章 - 可靠、可扩展与可维护的应用系统
      • 第2章 - 数据模型与查询语言
      • 第3章 - 数据存储与检索
      • 第4章 - 数据编码与演化
      • 第5章 - 数据复制
      • 第6章 - 数据分区
      • 第7章 - 事务

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

  • 当析构函数遇到多线程
  • 对象的创建
  • 对象的销毁

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

当析构函数遇到多线程

当一个对象能被多个线程同时看到,那么对象的销毁时机就会变得模糊不清,可能出现多种竞态条件(race condition):

  1. 在即将析构一个对象时,如何知道此时是否有别的线程正在执行该对象的成员函数?
  2. 如何保证在执行成员函数期间,对象不会被另一个线程析构?
  3. 在调用某个对象的成员函数之前,如何得知该对象还活着?它的析构函数会不会碰巧执行到一半? 解决这些race condition是C++多线程编程面临的基本问题?可以尝试通过智能指针shared_ptr解决上述问题。

对象的创建

对象构造要做到线程安全,唯一的要求是在构造期间不要泄露this指针,即:

  • 不要在构造函数中注册任何回调;
  • 不要在构造函数中将this指针传递给跨线程的对象;
  • 即使在构造函数最后一行也不行。

因为构造函数在执行期间对象还没有完成初始化,this指针泄露给其他对象,可能别的线程访问的是一个半成品。

对象的销毁

mutex只能保证成员函数顺次执行的安全,但是作为数据成员的mutex不能保护析构:

mutex不能保护析构

尽管线程A在销毁对象x之后把x设为NULL,尽管线程B在调用x之前检查了指针x地值,但是还是无法避免一种race condition:

  1. 线程A调用delete x,执行到析构函数(1),已经持有了互斥锁mutex_,即将继续向下执行;
  2. x并没有被线程A设为NULL,因此线程B能通过if(x),进入x->update(),将阻塞在(2)处;
  3. 之后因为析构函数销毁x地同时也将成员变量mutex_给销毁,因此(2)将会发生难以预料的结果(可能会永远阻塞在(2)处,也可能会进入“临界区”,然后core dump,或者发生更糟糕的情况)

结论**:作为class数据成员的mutex_只能同步本class的其他数据成员的读和写,不能保护安全的析构**。

对象的关系

在面向对象程序设计中,对象的关系主要有三种:composition、aggregation、association。

  • composition(组合/复合),对象x的生命期由唯一拥有者owner控制,owner析构时会把x同步析构。例如Engine是Car的一部分。脱离Car的Engine是没有实在意义的,即表示Part-of 关系。

  • aggregation(聚合),是一种特殊的联系,它表明了“部分”到“整体”的关系。例如Person有一个Address,但是Addess的存在是不依赖Person的,即表示Has-a关系。

  • association(关联/联系),是一种很宽泛的关系,它表示一个对象a用到了另一个对象b,即a持有b的指针(或引用),但是b的生命期不由a单独控制。

Association描述的是类与类之间一般的关联关系,而Aggregation和Composition都有整体和部分的关系。其中Aggregation中的部分脱离了整体,部分仍然有意义,是共享式的。而Composition中的部分脱离了整体,部分将没有任何意义,是独占式的。

从实现的角度上讲:

三者中以属性出现时,有着不同的实现方法。其中Association中作为属性出现时,不需要对其进行强制赋值,只要在使用时对其进行初始化即可。Aggregation中作为属性出现时,需要在构造器中通过传递参数来对其进行初始化。Composition作为属性出现时,需要在整体的构造器中创建部分的具体实例,完成对其的实例化。

原始指针和智能指针

指向对象的原始指针是坏的,尤其当这个指针暴露给其他线程的时候。从而,引入智能指针shared_ptr/weak_ptr的概念。shared_ptr是强引用,只要有一个指向x对象的shared_ptr存在,则该对象不会被析构;weak_ptr是弱引用,如果对象还存在,则可以提升为强引用来使用。

应用shared_ptr/weak_ptr可以解决上述x->update()的问题,因为不是通过加锁来控制,而是每次尝试提升,成功执行update逻辑。

应用到Observer

shared_ptr的线程安全问题:shared_ptr本身不是100%线程安全的,它的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr有两个数据成员,读写操作不能原子化。还是需要借助mutex来保护多线程情况下对同一个shared_ptr的访问。

官方说法:

Thread Safety

shared_ptr objects offer the same level of thread safety as built-in types. A shared_ptr instance can be "read" (accessed using only const operations) simultaneously by multiple threads. Different shared_ptr instances can be "written to" (accessed using mutable operations such as operator= or reset) simultaneously by multiple threads (even when these instances are copies, and share the same reference count underneath.)

Any other simultaneous accesses result in undefined behavior.

即shared_ptr 只能保证一个shared_ptr 实例被多个线程同时读取,多个shared_ptr 实例被多个线程同时写入是安全的,其他行为是未定义的。

shared_ptr是强引用,使用时可能会意外延长对象的生命期。shared_ptr在做函数参数时,可以考虑以const reference方式传播。

对象池

假定Stock类表示一只股票的价格,每只股票有唯一的字符串标识。例如Tencent的key是“SJS:Tencent”,Alibaba是“SJS:Alibaba”。即同一个程序中,每一只股票只有一个Stock对象,存在多处使用,那么Stock对象一个被共享。从而设计出如下StockFactory对象池。

对象池1

上述设计中get函数是从stocks_中根据key寻找对应的Stock对象,找到就返回stocks_[key];否则新建一个Stock,并存入stocks_[key]。使用shared_ptr会导致Stock对象永远不被销毁,因此应该使用weak_ptr。

对象池2

更进一步,利用shared_ptr的定制析构功能,在析构Stock对象的同时清理stocks_。

对象池3

因为get函数中使用了this指针,如果StockFactory的生命期比Stock短,则析构Stock时调用StockFactory::deleteStock就会core dump。此时,可以使用enable_shared_from_this。

对象池4

但是使用enable_shared_from_this后,StockFactory的生命期被延长了。下面,使用弱回调技术进行改进:

对象池5

总结

  1. 原始指针暴露给多个线程往往会造成race condition或额外的程序负担。
  2. 统一用shared_ptr/scoped_ptr来管理对象的生命期,在多线程中尤为重要。
  3. shared_ptr是值语意,当心意外延长对象的生命期。例如boost::bind和容器都可能拷贝shared_ptr。
  4. weak_ptr是shared_ptr的好搭档,可以用作弱回调、对象池等。 ​
Last Updated:
Contributors: klc407073648
Prev
消息广播服务