文章目录
索引
【Linux多线程服务端编程】| 【01】线程安全的对象生命期管理笔记
【Linux多线程服务端编程】| 【02】线程同步精要
【Linux多线程服务端编程】| 【03】多线程服务器的适用场合和常用编程模型
【Linux多线程服务端编程】| 【04】C++多线程系统编程精要
线程同步
1 互斥器 Mutex
【C++模块实现】| 【07】对于互斥、自旋锁、条件变量、信号量简介及封装
最常用,保存临界区
,任何时刻最多只能由一个线程在此mutex划出的临界区内活动;
【RAII封装mutex】:
- 只用
不可重入
; - 不手工调用上锁、解锁;
- 可以
防止
因加锁顺序
不同而导致死锁;
【次要原则】:
- 不适用
跨进程
的mutex,进程间通信只用在TCP sockets
; - 加锁、解锁在
同一线程
,其他线程不能去解其他线程的锁; - 必要时候使用
PTHREAD_MUTEX_ERRORCHECK
;
1.1 只使用不可重入的mutex
【可重入和不可重入的区别】:
- 同一线程可重复对
重入
加锁,不能对非重入
加锁; - 两者性能相差不大,只是少用一个
计数器
; - 在同一线程对非重入加锁会立刻导致
死锁
,能在调试中暴露问题; - 可重入可能会在代码中隐藏问题;
【可重入问题】:
- 若mutex为不可重入,则
死锁
; - 若mutex为可重入,则push_back可能导致vector
迭代器
失效,程序可能会瘫痪;
【若确实需要修改vector】:
- 可将
修改推后
,先记录
循环中要添加或删除的元素,循环结束后在修改; - 或使用
copy-on-write
后续介绍;
【建议】:若一个函数可能会在加锁或不加锁的情况下使用
- 定义一个跟原来一样的
同名函数
; - 但容易误用倒是死锁或数据损坏;
【在该函数上如何避免死锁或数据损坏】: - 【死锁】:后续;
- 【数据损坏】:在postWithLockHold加上
assert
,判断是否上锁;
1.2 死锁
【死锁中如何调试】:
https://www.cnblogs.com/zhuyp1015/p/3618863.html
【示例】:
【两个线程死锁】:
Inventory中的printAll可能回引发死锁;
【原由】:Inventory::printAll(#6)
调用Inventory::printAll(#5)
…而调用threadFunc线程先调用Request::~Request(#6)
在调用Inventory::remove(#5)
;两个序列加锁顺序相反导致的;
2 条件变量
mutex是用来排他性
地访问共享数据,不是等待原语
;
若需要等待某条件成立,则需要使用条件变量
;
【wait】:
- 必须与mutex使用,让条件读写
受mutex保护
; - 在mutex已上锁时才能调用wait,避免出现
虚假唤醒
; - 需要将条件和wait()置入while中;
【signal、broadcast】:
- 不一定要在mutex已上锁的情况下调用
signal
; - 在signal前要修改
条件变量
; - 修饰条件变量要用mutex保护;
- 注意区分signal(为资源可用)boradcast(为明状态变化);
【倒计时,同步用途】:
- 用于主线程等待
子线程
完成初始化(子线程完成一定任务后,主线程才执行); - 多个子线程等待主线程发出
起跑命令
; - 接口使用简单;
3 不要用读写锁和信号量
【读写锁容易错误】:
- 容易在有读锁时修改了
共享数据
; - 读写锁不见得比mutex更高效,由于读锁需要更新当前reader的数目;
- 一般
读
锁时可重入
,写锁不可重入
,为了预防写饥饿,写锁一般回阻塞后来的读锁,而读锁是可重入,会导致死锁; - 若对并发有极高性能,可考虑使用
read-copy-update
;
【信号量】:需要维护计数值
;
- 在维护的同时,回造成同样的两份数据,保持一直,易出错增加负担;
4 封装Mutex
该类class不允许拷贝构造
和赋值
;
【Mutex封装】:
MutexLockGuard(x)是为了防止错误使用;
【条件变量封装】:
存在mutex和条件变量,需要注意他的声明、初始化顺序
;
5 线程安全的Singleton实现
在此之前,提出过double checked locking
来解决单例问题,但还是有缺陷,可用使用pthread_once
;
使用pthread_once_t来保证lazy initialization的线程安全,可使用atexit
来提供销毁功能;
6 sleep(3)不是同步原语
生产中的线程等待:
- 等待资源可用
select/poll/epoll_wait
或条件变量
上; - 等待进入临界区以便读写共享数据;
若在等待某个事件要发生,一般采用select等或条件或线程池、倒计时、队列,而不是使用sleep;
7 借助shared_ptr实现copy-on-write
主要解决读多写少
的场景,当一个函数在读取时,我们对列表进行写入或删除
操作可能会引起不必要的麻烦,所以在写入时,我们应该在拷贝
一个副本
在副本上进行操作;
【原理】:
- write,若发生引用计数为1,则可安全
修改共享
; - read,在读前把引用计数加1,读完后减1保证读期间
引用计数大于1
,来阻止并发写
; - 主要:当write引用计数大于1,该如何解决;
【以下为解决1.1内容中的问题】
【解决1.2问题】
- 【做法一】:先将数据
拷贝
,在对齐进行遍历
- 【做法二】:用
shared_ptr管理set
,遍历时增加引用计数,阻止并发并修改;copy-on-write
;
【用mutex替换读写锁的场景】:
当有几个工作线程处理客户交易请求
(只读),另一个协程确保数据更新map
(读写),