muduo(一):C++多线程系统编程

2023-11-02

目录

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

对象的创建很简单

  • 对象构造要做到线程安全,唯一的要求是在构造期间不要泄露this指针(别的线程有可能访问这个半成品对象)。
  • 二段式构造——构造函数+initialize()——有时会是好办法。

销毁太难

  • 作为数据成员的mutex不能保护析构。另外,对于基类对象,调用到基类析构函数的时候,派生类对象的那部分已经析构了,那么基类对象拥有的MutexLock不能保护整个析构过程。
  • 只有当别的线程都访问不到这个对象时,析构才是安全的。
  • 一个函数如果要锁住相同类型的多个对象,为了保证始终按相同的顺序加锁,我们可以比较mutex对象的地址,始终先加锁地址较小的mutex。

线程安全的Observer有多难

  • 对象的关系主要有三种:组合、聚合、关联。
    • 组合:对象的生命期由其唯一的拥有者控制,拥有者析构的时候会把对象也析构掉。
    • 聚合:我有一个东西是从别人那里借来的。
    • 关联:对象a用到了另一个对象b,调用了后者的成员函数。
  • 如果对象x注册了任何非静态成员函数回调,那么必然在某处持有了指向x的指针,这就暴露在race condition之下。
  • 直接使用shared_ptr会形成循环引用,直接造成资源泄露。
  • shared_ptr/weak_ptr的计数在主流平台上是原子操作,没有用锁,性能不俗。
  • 垃圾回收的原理:所有人都用不到的东西一定是垃圾。
  • 内存碎片:
    • 内存碎片:进程不能完全使用分给它的固定内存区域。
    • 外存碎片:未分配的连续内存区域大小。

再论shared_ptr的线程安全

  • 它的引用计数本身是安全且无锁的,但对象的读写不是。
  • 要在多个线程中同时访问同一个shared_ptr,正确的做法是用mutex保护。
  • 用一个指向同一对象的栈上shared_ptr local copy:缩短了临界区长度。

shared_ptr技术与陷阱

  • bind会把实参拷贝一份,延长对象的生命期。
  • 一个线程只需要在最外层函数有一个实体对象,之后都可以用const reference来使用这个shared_ptr。
  • 对象的析构是同步的,对象会在同一个线程析构,这个线程不一定是对象诞生的线程。可以用一个单独的线程来专门做析构,通过一个BlockingQueue<shared_ptr>把对象的析构都转移到那个专用线程。
  • 避免循环引用:owner持有指向child的shared_ptr,child持有指向owner的shared_ptr。

对象池

  • stocks的大小只增不减:利用shared_ptr的定制析构功能。
  • shared_from_this():将this指针变身位shared_ptr。

弱回调

  • 利用weak_ptr,在回调的时候先尝试提升位shared_ptr(lock),如果提升成功,说明接受回调的对象还健在,那么就执行回调。
  • 没有垃圾回收的并发编程是困难的。

第2章 线程同步精要

互斥器

  • 用RAII手法封装mutex:保证锁的生效期间等于一个作用域,不会因异常而忘记解锁。(java synchronized)
  • 由于Guard对象是栈上对象,看函数调用栈就能分析用锁的情况,非常便利。
  • 进程间通信只用TCP socket。

只使用非递归的mutex

  • 区别:同一个线程可以重复对可重入锁加锁,,但是不能重复对不可重入锁加锁。
  • 优越性:把程序的逻辑错误暴露出来。
  • Linux的Pthreads mutex不必每次加锁、解锁都陷入系统调用,效率不错。在多CPU系统上,如果不能立刻拿到锁,它会先spin以小段时间,如果还不能拿到锁,才挂起当前线程。

false sharing(转载)

  • 伪共享的非标准定义为:缓存系统中是以缓存行(cache line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
  • MESI协议保证缓存的相干性和内存的相干性。
  • 远程写时,同时处理RFO请求以及设置I的过程将给写操作带来很大的性能消耗。

spurious wakeup

  • 虚假唤醒:pthread_cond_signal可能唤醒多个线程,必须用while循环来等待条件变量。

不要用读写锁和信号量

  • 一种易犯的错误是在持有read lock的时候修改了共享数据。
  • 无论如何read lock加锁的开销不会比mutex lock小。如果临界区很小,锁竞争不激烈,那么mutex往往会更快。
  • 写锁优先,会阻塞后面的读锁(可以用shared_ptr实现copy-on-write)。

static_assert

  • 静态断言,用来做编译期间的断言,不会造成任何运行期性能损失。
  • assert应该捕捉不应该发生的非法情况。不能用assert检查返回值,因为在realise build里面是空语句。

sleep(3)不是同步原语

  • 只能常出现在测试代码中。
  • 在用户态做轮询(polling)是低效的。

借shared_ptr实现copy-on-write

  • 对于write端,如果发现引用计数为1,可以安全地修改共享对象;不为1,拷贝一份(read端此时读的是旧的对象)。
  • 对于read端,在读之前把引用计数加1,读完之后减1,保证在读的期间引用大于1。

第3章 多线程服务器的使用场合与常用编程模型

推荐模式

  • one loop per thread + thread pool。

进程间通信只用TCP

  • 可以跨主机,具有伸缩性。
  • TCP port由一个进程独占,且操作系统会自动回收。即使程序意外退出,也不会给系统留下垃圾。
  • 跨语言,服务器和客户端不必使用同一种语言。
  • 可记录、可重现(tcpdump)。
  • 容易定位服务之间的依赖关系。

必须用单线程的场合

  • 只有单线程程序能fork。
  • 单线程程序能限制程序的CPU占用率,防止非关键任务耗尽CPU资源。
  • Even loop是非抢占式的,这个缺点可以用多线程来克服。
  • 计算/IO密集型,多线程都没有什么绝对意义上的性能优势(Web服务器、subset sum)。
  • 任何一方早早地先到达瓶颈,多线程程序都没啥优势。

适用多线程程序的场景

  • 多线程不能提高绝对性能,但能提高平均响应性能(IO和计算相互重叠)。
  • 有多个CPU可用。
  • 线程间有共享数据。
  • 共享的数据是可以修改的。
  • 提供非均质的服务。

round-robin

  • 轮询调度算法,是一种无状态调度。

多线程能提高吞吐量吗

  • 如果用thread per request的模型,每个客户请求用一个线程去处理,那么当并发请求数大于某个临界区时,吞吐量反而会下降。

如何让IO和计算相互重叠

  • 基本思路是:把IO操作通过BlockingQueue交给别的线程去做,自己不必等待。

线程池的大小的阻抗匹配原则

  • T个线程,每个线程占有P的CPU事件,如果刚好占满C个CPU,那么必有T*P=C。

第4章 C++多线程系统编程精要

Linux上的线程标识

  • pthread_t:进程内唯一的,不同进程内可能相同。pthread的值很容易重复。

  • pid_t:全局唯一,不同进程内也不相同。而且是采用递增轮回法分配,短时间内启动多个线程也会具有不同的线程id。

  • 建议使用gettid系统调用的返回值作为线程id。

  • Current::tid使用__thread变量来缓存gettid的返回值。避免子进程fork后看到缓存,用pthread_atfork()注册一个回调来清空缓存。

线程的创建与销毁的守则

  • 程序不应该在未提前告知的情况下创建自己的“背景线程”。这样程序可以统筹线程的数目和用途,避免低优先级的任务独占某个线程。
  • 尽量用相同的方式创建线程。容易做一些统一的簿记工作。
  • 在进入main()函数之前不应该启动线程。避免影响全局对象的安全构造(C++保证在进入main()之前完成全局对象的构造)。
  • 程序中线程的创建最好在初始化阶段全部完成。
  • 任何从外部强行终止线程的做法和想法都是错的。

pthread_cancel与C++

  • 线程不是执行到此函数就立刻终止,而是该函数会抛出异常。这样可以有机会执行stack unwind。

exit在C++中不是线程安全的

  • exit会析构全局对象和static对象,不析构局部对象(不存在栈空间回收的问题)。因此可能造成死锁,析构函数的竟态条件。
  • 可以考虑用_exit系统调用。它不会试图析构全局对象,但是也不会执行其他任何清理工作。

善用__thread与关键字

  • __thread是GCC内置的线程局部存储设施,比pthread_key_t快很多,存取效率可与全局变量相比。
  • __thread无法自动调用构造函数和析构函数,只能用于修饰全局变量和静态变量,初始化只能用编译期常量。

多线程与IO

  • 操作文件描述符的系统调用本身是线程安全的。
  • socket读写的特点是不保证完整性。
  • 多个线程分别read或write同一个磁盘上的多个文件也不见得能提速。因为每块磁盘都有一个操作队列,多个线程的读写请求到了内核是排队执行的。只有在内核缓存了大部分数据的情况下,多线程读这些热数据才可能比单线程快。
  • 每个文件描述符只由一个线程操作,从而轻松解决消息收发的顺序性问题,也避免了关闭文件描述符的各种race condition。
  • 对于磁盘文件,在必要的时候多个线程可以同时调用pread/pwrite(相当于先调用lseek)来读写同一个文件。
  • 对于UDP,由于协议本身保证消息的原子性,可以多个线程同时读写同一个UDP文件描述符。

用RAII包装文件描述符

  • POSIX标准要求每次新打开文件的时候必须使用当前最小可用的文件描述符号码,这种分配方式可能导致串话。因此不应该stdout或stderr,正确的做法是把stdout或stderr重定向到磁盘文件。
  • 用全局表来避免串话通常意味着每次读写都要对全局表加锁。
  • RAII:用Socket对象包装文件描述符,所有对此文件描述符的读写都通过此对象进行,在对象的析构函数里关闭文件描述符。
  • 为了防止访问失效的对象或者发生网络串话,muduo使用shared_ptr来管理TcpConnection的生命期。

RAII与fork()

  • 用RAII手法管理子进程未继承的资源时(定时器、内存锁、文件锁等等),fork出来的子进程不一定正常工作。

多线程与fork()

  • Linux的fork()只克隆当前线程的thread of control,不克隆其他线程。fork()后,其他线程可能正好位于临界区之内,持有了某把锁,而它突然死亡,再也没有机会去解锁了。

    因此子进程不能调用:malloc、任何可能分配和释放内存的函数、printf系列函数、任何Pthreads函数。

  • 唯一安全的做法是在fork()之后立即调用exec()执行另一个程序。

多线程与signal

  • 在signal handler中只能调用可重入函数,不是每个线程安全的函数都是可重入的。
  • 在signal handler中不能调用任何Pthreads函数。
  • 如果signal handler中需要修改全局数据,那么被修改的变量必须是sig_atomic_t类型的。因为编译器有可能假定这个变量不会被他处修改,从而优化了内存访问。
  • 使用signal的第一原则是不要使用signal
  • 也不要使用基于signal实现的定时函数。
  • 不主动处理各种异常信号。
  • 在没有别的替代方法的情况下,使用signalfd统一事件源。

第5章 高效的多线程日志

功能需求

  • 调整日志的输出级别不需要重新编译,也不需要重启进程。

  • 应该避免往网络文件系统(例如NFS)上写日志,这等于掩耳盗铃。

  • 以本地文件为日志的destination,日志文件的滚动是必需的。

  • 万一程序崩溃,那么最后若干条日志往往就丢失了。

    muduo日志库用两个办法来应对这一点:其一是定期将缓冲区内的日志消息flush到硬盘;其二是每条内存中的日志消息都带有cookie,其值为某个函数的地址,这样通过在core dump文件中查找cookie就能找到尚未来得及写入磁盘的消息。

多线程异步日志

  • 一个多线程程序的每个进程最好只写一个日志文件。

  • “异步日志”:用一个背景线程负责收集日志消息,并写入日志文件,其他业务线程只管往这个“日志线程”发送日志消息。(防止阻塞)

  • 双缓冲技术:在新建日志消息的时候不必等待磁盘文件操作,也避免每条新消息都触发后端日志线程(相当于于批处理)。

  • nextBuffer_可以减少前端临界区分配内存的概率,缩短前端临界区长度。

  • 这四个缓冲在程序启动的时候会全部填充为0,这样可以避免程序热身时page fault引发性能不稳定。

  • page fault(转载):

    • major page fault也称为hard page fault, 指需要访问的内存不在虚拟地址空间,也不在物理内存中,需要从慢速设备载入。从swap回到物理内存也是hard page fault。
    • minor page fault也称为soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。 (通常是多个进程访问同一个共享内存中的数据,可能某些进程还没有建立起映射关系,所以访问时会出现soft page fault)
    • invalid fault也称为segment fault, 指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问,内核会报segment fault错误。
  • 日志消息堆积:对于异步日志来说,生产速度高于消费速度,会造成数据在内存中堆积。解决办法是直接丢掉多余的日志buffer。

其他方案

  • 用多个桶子,前端写日志的时候再按线程id哈希到不同的bucket中,以减少contention。
  • muduo日志库不允许指定路径:在启动脚本(shell脚本)里改变当前路径。
  • 通过sysctl设置参数,让每次core dump都产生不同的文件。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

muduo(一):C++多线程系统编程 的相关文章

  • Nginx access日志配置

    Nginx access日志配置 access log日志配置 access log用来定义日志级别 日志位置 语法如下 日志级别 debug gt info gt notice gt warn gt error gt crit gt al
  • vscode 中,vue导入组件路径提示

    VsCode中 Vue导入组件路径提示 当用Vue组件开发时 经常会引入文件 但又没提示该如何解决 注意 编写vue项目时需要从根目录打开 否则会导致很多插件不能用 注意 设置完成展示 打开VSCode 第一步 点击扩展 第二步 搜索框输入
  • 接口日志记录

    1 添加配置 保证日志记录信息类能执行
  • Bitcoin的Segwit地址

    Segwit地址又称隔离见证地址 在Bitcoin Blockchain上 经常可以看到类似bc1qmy63mjadtw8nhzl69ukdepwzsyvv4yex5qlmkd这样的以bc开头的地址 这种地址就是隔离见证地址 Segwit地
  • 汽车年检记录

    车子已经两年了 需要年检 早就听说年检挺麻烦的 要花不少时间 4S店也可以代为年检 不过第一次年检 我想自己体验一下 于是决定还是自己去年检 下面是年检的过程 备忘一下 另外我是去 浙江省杭州市市区浙江省杭州市西溪路529号 第一检测站进行

随机推荐

  • java并发编程(荣耀典藏版)

    大家好 我是月夜枫 聊一聊java中的并发编程 面试工作中也许都会用到 参考了很大博主的博客 整理了很久的文章 虽然还没有全部整理完 后续慢慢更新吧 并发编程 一 线程的基础概念 一 基础概念 1 1 进程与线程A 什么是进程 进程是指运行
  • 嵌入式LINUX环境搭建 - 写给刚入行的童学

    转自百问网论坛 www 100ask org 1 虚拟机里面的Linux无法上网 注意四个地方即可 第3点特别重要 很多同学的Linux不能上网就是因为这里 一定要选对网卡 千万别选自动 因为它还不够智能 如果你的物理机在用网线进行上网 那
  • C# 结构体(学习心得 16)

    结构体 是 值类型 数据结构 使单一变量可以存储各种数据类型的相关数据 struct 关键字用于创建结构体 结构体用来代表一个记录 超级小白友好 讲解C 基础 每集5分钟轻松学习 拒绝从入门到放弃 一 定义结构体 声明方式 struct B
  • 新知同享

    谷歌致力于通过高效 可靠的方法 构建 AI 驱动的产品 如今已经走过了七年 AI 为先 的旅程 一起来看 2023 Google 开发者大会上 AI 开发如何被广泛应用 简化开发 并将机器学习的强大能力 引入到应用和工作流中 提高开发者工作
  • 离线安装docker,docker安装MySQL,Redis,ES,Kibana,mongoDB,RocketMQ

    目录 安装docker docker compose 离线安装docker docker compose 安装ElasticSearch 安装kibana 下载ik分词器 安装MySQL 设置MySQL主从 启动单机版MySQL可以不做这一
  • 搬砖时遇到的错误

    Unreachable code 我把下面两个写反了位置 下图是正确写法
  • Java中throw、throws和Throwable的联系与区别

    throw 意为 投掷 抛 扔 Throw Throws和Throwable三者都用于异常处理 1 Throwable Throwable在Java中是异常处理这个分支的顶级父类 其它所有异常处理的实现都依赖于Throwable 打开Jav
  • 进阶sql语句

    div class markdown views div
  • 【C语言】反序加密(非文件)

    算法集训传送门 引言 铭记于心 我唯一知道的 便是我一无所知 我们的算法之路 众所周知 作为一名合格的程序员 算法 能力 是不可获缺的 并且在算法学习的过程中我们总是能感受到算法的 魅力 短短几行代码 凝聚无数前人智慧 一个普通循环 即是解
  • 挂载nfs报错合集

    问题一 报错 VFS Unable to mount root fs via NFS trying floppy 83 929713 random fast init done 100 366640 VFS Unable to mount
  • LOL代练检测——2019腾讯游戏安全技术竞赛初赛记录

    因为想要去腾讯实习 所以参加了今年的腾讯游戏安全技术竞赛 这个比赛赢了会有一个实习的绿色通道 选了数据分析方向里面的机器学习 初赛题目很有趣 关于LOL 英雄联盟 的代练检测 Moba游戏常见的5v5模式 玩家将与其余9名玩家共同组成对局
  • python 群发邮件 亲测可用

    1 先开通邮件的POP3 SMTP IMAP 拿到授权密码 注意 不是邮箱密码 2 相关代码 里面涉及到的数据库和邮件 都是相对路径下的 可根据代码自行创建和维护 pip install PyEmail import smtplib smt
  • 瑞芯微RGMII的配置

    主要配置项 除去复位等信号 我们主要关注两大块的配置 时钟配置 MAC 采用125M时钟 PHY采用25M时钟 主要配置时钟源 这个和具体硬件连线强相关 例如125M时钟可以来源于soc内部的PLL 也可以由对端PHY 提供 由对端PHY
  • 暴力解法破解PTA L1-006 连续因子 (20分)

    L1 006 连续因子 20分 一个正整数 N 的因子中可能存在若干连续的数字 例如 630 可以分解为 3 5 6 7 其中 5 6 7 就是 3 个连续的数字 给定任一正整数 N 要求编写程序求出最长连续因子的个数 并输出最小的连续因子
  • JetBrains使用Edu邮箱免费注册

    参考 https blog csdn net qq 22070551 article details 88864870 主要需要注意的是下面的问题 有一种情况无法激活就是 提示 jetbrains account connection er
  • 将类的非静态成员函数作为函数指针传递

    项目场景 QT C C 混合编程 将类的成员函数作为函数指针传递 问题描述 无法将类的成员函数作为函数指针传递给C语言的结构体 协议由C语言开发 结构体如下所示 DLT645 环境结构体 typedef struct dlt645 uint
  • pandas dataframe 一列数据变多列

    一 问题描述 希望把dataframe 中的一列数据分割为多列追加到dataframes上 分割前示意图 分割后效果图 二 代码实现 提取出分割列的名称 或者可以自己定义分割后的列名称 feature name data columns 1
  • ChatGPT是否可以生成虚拟角色?

    ChatGPT可以生成虚拟角色 作为一种基于人工智能的自然语言处理技术 ChatGPT具有广泛的应用领域 其中包括虚拟角色生成 虚拟角色是指在虚拟世界中代表和扮演特定角色的程序或实体 可以通过软件 硬件等方式进行构造和操作 虚拟角色通常包括
  • Quoit Design ————分治与归并(平面分治模板)

    Have you ever played quoit in a playground Quoit is a game in which flat rings are pitched at some toys with all the toy
  • muduo(一):C++多线程系统编程

    目录 第1章 线程安全的对象生命期管理 对象的创建很简单 销毁太难 线程安全的Observer有多难 再论shared ptr的线程安全 shared ptr技术与陷阱 对象池 弱回调 第2章 线程同步精要 互斥器 只使用非递归的mutex