Mudo C++网络库第二章学习笔记
线程同步的精要
- 并发有两种基本的模型:
- 一种是message passing(消息传递);
- 另一种是shared memory(共享内存);
- 在分布式系统中(有多台物理机需要通信), 运行在多台机器上的多个进程只有一种实用模型:message passing(消息传递), 因为多个物理机基本上不能共享内存;
- 并发(concurrency);
- 线程同步的四项原则, 按重要性排列:
- 首要原则是尽量最低限度地共享对象, 减少需要同步的场合;
- 一个对象能不暴露给别的线程就不要暴露;
- 如果暴露就优先考虑immutable对象(const);
- 实在不行才暴露可修改的对象,并用同步措施来充分保护它;n
- 其次, 使用高级的并发编程构件, 如任务队列(TaskQueue), 生产者消费者队列(Producer-Consumer Queue), 闭锁(CountDownLatch)等;
- 最后不得已才使用底层同步原语(primitives)时, 只用非递归的互斥器和条件变量, 慎用读写锁, 不要用信号量;
- 除了使用atomic整数之外, 不要自己编写lock-free代码, 也不要用"内核级"同步原语; 不能凭空猜测'那种做法性能会更好', 比如spin lock(自旋锁) vs mutex(互斥量);
- 首要原则是尽量最低限度地共享对象, 减少需要同步的场合;
互斥量(mutex)
- 主要是为了保护共享数据:
- 用RAII(Resource Acquisition Is Initialization -- 资源申请即初始化)手法封装mutex的创建, 销毁, 加锁, 解锁这四个操作;
- 保证锁的生效期间等于一个作用域(scope), 不会因异常而忘记解锁;
- 用RAII(Resource Acquisition Is Initialization -- 资源申请即初始化)手法封装mutex的创建, 销毁, 加锁, 解锁这四个操作;
- 只用非递归的mutex(即不可重入的mutex);
- mutex分为递归(recursive)和非递归(non-recursive)两种, 这是POSIX的叫法, 另外的名字是可重入(reentrant)和非可重入;
- 这两种mutex对线程间(inter-thread)的同步基本没有区别, 他们的唯一区别就是: 同一线程可以重复对recursive mutex加锁, 但是不能重复对non-recursive mutex加锁;
- recursive mutex(可重入的互斥量)可能会隐藏代码里的一些问题:
- 典型情况是, 以为拿到一把锁就可以修改对象了, 没想到外层代码已经拿到了锁, 正在修改(或读取)同一个对象呢;
- mutex分为递归(recursive)和非递归(non-recursive)两种, 这是POSIX的叫法, 另外的名字是可重入(reentrant)和非可重入;
- 不手工调用lock()和unlock()函数, 一切交给栈上的Guard对象的构造和析构函数负责;
- 这种做法叫做Scoped Locking(作用域加锁);
- 在每次构造Guard对象时, 思考一路上(调用栈上)已经持有的锁, 防止因加锁顺序不同而导致死锁(deadlock);
- 由于Guard对象是栈上对象, 看函数调用栈就能分析用锁的情况, 非常便利;
- 不使用跨进程的mutex, 进程间通信只用TCP sockets;
- 加锁和解锁在同一个线程, 线程a不能去unlock线程b已锁住的mutex(RAII自动保证);
- 不要忘记解锁(RAII自动保证);
- 不重复解锁(RAII自动保证);
- 必要时可以考虑用PTHREAD_MUTEX_ERRORCHECK来排错;
- pthread_atfork()函数讲解
- GCC支持的无锁化编程
- 利用
thread apply all bt
命令可以在gdb调试中查看所有的堆栈调用信息;
死锁(dead lock)
- 坚持使用Scoped Locking(作用域加锁), 很容易在出现死锁的时候定位bug;
条件变量(condition variable)
- 互斥器(mutex), 是加锁原语, 用来排他性地访问共享数据, 它不是等待原语;
- 如果需要等待某个条件成立, 应该使用条件变量(condition variable);
- 条件变量是一个或多线程等待某个布尔表达式为真, 即等待别的线程唤醒它;
- 条件变量的学名叫管程(monitor);
- 条件变量只有一种使用方式, 几乎不可能用错:
- 对于wait端:
- 必须与mutex一起使用, 该布尔表达式的读写虚受此mutex保护;
- 在mutex已上锁的时候才能调用wait()函数;
- 把判断布尔条件和wait()放到while循环中;
- 对于signal/broadcast端:
- 不一定要在mutex已上锁的情况下调用signal(理论上);
- 在signal之前一般要修改布尔表达式;
- 修改布尔表达式通常要用mutex保护(至少用作full memory barrier);
- 注意区分signal和broadcast:
- broadcast通常用于表明状态变化;
- signal通常用于表示资源可用;
- 对于wait端:
- 条件变量是非常底层的同步原语, 很少直接使用一般都是用它来实现高层的同步措施, 如BlockingQueue或CountDownLatch(倒计时器);
- 倒计时器(CoutDownLatch)是一种常用且易用的同步手段, 主要有两种用途:
- 用于主线程等待多个子线程完成初始化;
- 用于多个子线程等待主线程发起"起跑"命令;
- 倒计时器(CoutDownLatch)是一种常用且易用的同步手段, 主要有两种用途:
- 互斥量和条件变量构成了多线程编程的全部必备同步原语, 用它们即可完成任何线程同步任务, 二者不能相互代替;
- 千万不要连mutex都没有学会、用好,一上来就考虑lock-free设计;
不要用读写锁和信号量
- 读写锁(Readers-Writer lock, 简写为rwlock)是个看上去很美的抽象, 它明确区分了read和write两种行为;
- 首选rwlock来保护共享状态, 这不见得是正确的;
- 从正确性方面来说, 在持有read lock的时候修改了共享数据,在程序维护阶段容易犯的错误;
- 从性能方面来说, 读写锁不一定比普通的mutex更高效, 读写锁要更新当前reader的数目, 如果临界区很小, 锁竞争不激烈, 那么mutex会更快;
- reader lock允许提升为writer lock, 也可能不允许提升;
- 通常reader lock是可重入的, writer lock是不可重入的, 但是为了防止writer lock饥饿, writer lock通常会阻塞后来的reader lock, 所以reader lock在重入的时候可能死锁;
- 追求低延迟的读取场合也不适合读写锁;
- 如果确实并发读写有极高的性能要求, 可以考虑read-copy-update;
- 首选rwlock来保护共享状态, 这不见得是正确的;
封装Mutexlock、MutexLockGuard、Condition类
- 这几个类都不允许拷贝构造和赋值;
- 用mutexattr来显示指定mutex的类型;
- 检查返回值尽量不用assert函数, 因为assert函数在release build里是空语句;
- 需要non-debug的assert时, 或许google-glog的CHECK宏是一个不错的思路;
- muduo库的特点是只提供最常用、最基本的功能, 特别有意避免提供多种功能近似的选择;
- muduo库删繁就简, 举重若轻;
- trylock函数的一个用途是用来观察lock contention;
- 提供灵活性固然是本事, 然而在不需要灵活性的地方把代码写死, 更需要大智慧;
- 一个多线程程序如果大量使用mutex和condition variable来同步, 基本跟用铅笔刀锯大树没啥区别;
线程安全的singleton实现
- double checked locking(DCL)兼顾了效率与正确性;
- 但有神牛指出由于乱序执行的影响, DCL是靠不住的;
- C++ 实现要么次次锁, 要么eager initialize(饿的单例), 或者动用memory barrier这样的大杀器;
- 在实践中, 用pthread_once就行(保证函数只执行一次);
- 用pthread_once_t来保证lazy-initialization的线程安全, 线程安全由pthread库保证;
sleep不是同步原语
- sleep, usleep, nanosleep只能出现在测试代码中, 比如写单元测试的时候;
- 或者用于有意延长临界区, 加速复现死锁的情况;
- sleep不具备memory barrier语义, 它不保证内存的可见性;
- 生产代码中线程的等待可分为两种:
- 一种是等待资源可用(要么等在select/poll/epoll_wait上, 要么等在条件变量上);
- 一种是等着进入临界区(等在mutex上), 以便读写共享数据; 这种等待极短, 否则程序性能和伸缩性就会有问题;
- 在程序的正常执行中, 如果需要等待一段已知的时间, 应该向event loop里注册一个timer, 然后在timer的回调函数里接着干活, 因为线程是个珍贵的共享资源, 不能轻易浪费(阻塞也是浪费);
- 如果等待某个事件发生, 那么应该采用条件变量或IO事件回调, 不能用sleep来轮询;
- 如果多线程的安全性和效率要靠代码主动调用sleep来保证, 这显然是设计出了问题;
- 等待某个事件发生, 正确的做法是用select等价物或condition, 抑或(更理想的)高层同步工具;
- 在用户态做轮询(polling)是低效的;
归纳与总结
- 线程同步的四原则, 尽量用高层同步设施(线程池, 队列, 倒计时);
- 使用普通互斥量和条件变量完成剩余的同步任务, 采用RAII惯用手法(idiom)和Scoped Locking(作用域加锁);
- 用好这几样东西, 基本上能应付多线程服务端的各种场合;
- 让正确的程序变快远比让一个快的程序变正确容易得多;
- 有些高级语言通过framework来屏蔽多线程, 让多线程看起来像是写单线程程序(Java Servlet);
- 掌握多线程编程, 才能更理智地选择用还是不用多线程, 因为你能预估多线程实现的难度与收益;
- 把一个单线程程序改成多线程, 往往比从头实现一个多线程更困难;
- 掌握同步原语和他们的使用场合是多线程编程的基本功;
- Unix的signal在多线程下的行为比较复杂, 一般要靠底层的网络库(如Reactor)加以屏蔽, 避免干扰上层应用程序的开发;
- 不能听信传言或是凭感觉优化;
- 真正影响锁性能的不是锁, 而是锁争用(lock condition);
- 在分布式系统中, 多机伸缩性(scale out)比单机性能优化更值得投入精力;
借shared_ptr实现copy-on-write
- 用shared_ptr来管理共享数据;
- shared_ptr 是引用计数型智能指针, 如果当前只有一个观察者, 那么引用计数的值为1;
- 对于write端, 如果发现引用计数为1, 这时可以安全的修改共享对象, 不必担心有人正在读它;
- 对于read端, 在读之前把引用技术加1, 读完之后减1, 这样保证在读的期间其引用计数大于1, 可以阻止并发写;
- 比较难的是write端, 如果发现引用计数大于1, 该如何处理, sleep一小段时间肯定是错的;
- 一般不能在原来上面修改, 得创建一个副本, 在副本上修改, 修改完了再替换;
- 如果没有用户在读, 那么就直接修改, 节约一次拷贝;
Mudo C++网络库第二章学习笔记的更多相关文章
- Mudo C++网络库第八章学习笔记
muduo网络库的设计与实现 muduo是基于Reactor模式的C++网络库; Reactor的关键结构 Reactor最核心的是事件分发机制, 即将IO multiplexing拿到IO事件分发给 ...
- Mudo C++网络库第十章学习笔记
C++编译链接精要 C++语言的三大约束: 与C兼容, 零开销(zero overhead)原则, 值语义; 兼容C语言的编译模型与运行模型, 也就是锁能直接使用C语言的头文件和库; 头文件包含具有传 ...
- AS开发实战第二章学习笔记——其他
第二章学习笔记(1.19-1.22)像素Android支持的像素单位主要有px(像素).in(英寸).mm(毫米).pt(磅,1/72英寸).dp(与设备无关的显示单位).dip(就是dp).sp(用 ...
- #Spring实战第二章学习笔记————装配Bean
Spring实战第二章学习笔记----装配Bean 创建应用对象之间协作关系的行为通常称为装配(wiring).这也是依赖注入(DI)的本质. Spring配置的可选方案 当描述bean如何被装配时, ...
- Python核心编程第三版第二章学习笔记
第二章 网络编程 1.学习笔记 2.课后习题 答案是按照自己理解和查阅资料来的,不保证正确性.如由错误欢迎指出,谢谢 1. 套接字:A network socket is an endpoint of ...
- 《Linux内核设计与实现》课本第一章&第二章学习笔记
<Linux内核设计与实现>课本学习笔记 By20135203齐岳 一.Linux内核简介 Unix内核的特点 Unix很简洁,所提供的系统调用都有很明确的设计目的. Unix中一切皆文件 ...
- Day2 《机器学习》第二章学习笔记
这一章应该算是比价了理论的一章,我有些概率论基础,不过起初有些地方还是没看多大懂.其中有些公式的定义和模型误差的推导应该还是很眼熟的,就是之前在概率论课上提过的,不过有些模糊了,当时课上学得比较浅. ...
- Linux第一章第二章学习笔记
第一章 Linux内核简介 1.1 Unix的历史 它是现存操作系统中最强大最优秀的系统. 设计简洁,在发布时提供原代码. 所有东西都被当做文件对待. Unix的内核和其他相关软件是用C语言编写而成的 ...
- Machine Learning In Action 第二章学习笔记: kNN算法
本文主要记录<Machine Learning In Action>中第二章的内容.书中以两个具体实例来介绍kNN(k nearest neighbors),分别是: 约会对象预测 手写数 ...
随机推荐
- spring boot集成redis的血泪史
首先说明环境不是我搭建的,然后因项目需要添加redis的时候,麻烦来了.springboot 用的是1.5.9因为以前弄过redis,所以直接拿过来,麻烦了首先是莫名的错误,连项目都启动不了.但是最后 ...
- java项目中文件含义
1. java项目 .project:是工程构建配置文件 .classpath:保存的是项目所用的外部引用包的路径 .settings:记录项目配置变化的记录文件夹 src:sourcefolder项 ...
- python-常用数据类型
九 基本数据类型 什么是数据?为何要有多种类型的数据? #数据即变量的值,如age=18,18则是我们保存的数据. #变量的是用来反映/保持状态以及状态变化的,毫无疑问针对不同的状态就应该用不同类型的 ...
- SQL Server进阶 SQL优化
找到消耗内存最多的SQL SELECT mg.granted_memory_kb, mg.session_id, t.text, qp.query_plan FROM sys.dm_exec_quer ...
- 29. SpringBoot Redis 非注解
1. 引入依赖 <parent> <groupId>org.springframework.boot</groupId> <artifactId>spr ...
- 最棒的 JavaScript 学习指南(2018版)
译者注:原文作者研究了近2.4万篇 JavaScript 文章得出这篇总结,全文包含学习指南.新人上手.Webpack.性能.基础概念.函数式编程.面试.教程案例.Async Await.并发.V8. ...
- Linux文件权限设置
基本概念 https://linux.cn/article-7418-1.html#3_8880 用户管理 文件权限设置 -添加用户账户08% -理解 /etc/passwd 中的内容12% -理解 ...
- [译]Debug ASP.NET Core 2.0源代码
原文 首先你的VS必须为VS 2017 15.3或以上版本. 打开你的Startup类,在ConfigureServices方法上设置个断点,按F5 Debug应用. 在Call Stack(调用堆栈 ...
- 【51nod 1100】斜率最大
Description 平面上有N个点,任意2个点确定一条直线,求出所有这些直线中,斜率最大的那条直线所通过的两个点. (点的编号为1-N,如果有多条直线斜率相等,则输出所有结果,按照点的X轴坐标 ...
- node.js 环境
Centos 7.2 安装 Node.js 环境 Node.js 是运行在服务端的 JavaScript, 是基于 Chrome JavaScript V8 引擎建立的平台. 1. Node.js w ...