线程同步的几种方式 前言 进程有自己的独立地址空间,因此进程之间重点通信,通信方式包括:管道Pipe、命名管道FIFO、消息队列MessageQueue、共享存储SharedMemory、信号量Semaphore、套接字Socket和信号Signal。 线程除了线程栈外其他数据都是共享的,如果同时读写数据可能造成数据不一致甚至程序崩溃的后果,因此线程之间重点同步。 1. 竞争条件 在多线程并发场景下指令执行的先后顺序由内核决定,同一个线程内部指令按照先后顺序执行,但不同线程之间的指令执行先后顺序是不一定的。 如果执行结果依赖于不同线程执行的先后顺序,那么就会形成“竞争条件”,由于竞争条件下计算结果是非预期的,因此我们应该尽量避免竞争条件的形成。 最常解决竞争条件的方式是原子操作,其次便是线程同步。 2. 线程同步概念 线程同步的概念和其他“同步”不太一致:设备同步:在不同的设备之间规定一个共同的参考时间数据库/文件同步:在不同的数据库之间保持数据一致 线程同步指的是线程之间“协同”,即线程之间按照规定的先后次序运行。 3. 线程同步方式 线程同步主要包括四种方式:互斥量读写锁条件变量信号量 互斥锁 1. 简介 互斥锁(又名互斥量)强调的是资源之间的访问互斥:每个线程在对共享资源操作前都会尝试先加锁,加锁成功才能操作,操作结束之后解锁。 某个线程对互斥量加锁后,任何其他试图再对互斥量加锁的线程都将被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态。第一个变成运行状态的线程可以对互斥量加锁,其余线程将会看到互斥量依然被锁住,只能回去再次等待它重新变为可用。 2. 特性 mutex是睡眠等待(sleep waiting)类型的锁,当线程抢互斥锁失败的时候,线程会陷入休眠。优点就是节省CPU资源,缺点就是休眠唤醒会消耗一点时间。 另外自从Linux 2.6版以后,mutex完全用futex的API实现了,内部系统调用的开销大大减小。 3. 主要接口 声明和销毁: 加锁解锁: 超时加锁,避免死锁: 4. 例子 读写锁 1. 简介 读写锁和互斥量类似,是另一种实现线程同步的方式,但是它将操作分为读、写两种方式,可以多个线程同时占用读模式,这样使得读写锁具有更高的并行性。相较于互斥锁而言读写锁有一定的性能提升,应对的是单写多读模型:写独占:写锁占用时,其他线程加读锁或者写锁时都会阻塞(并非失败)读共享:读锁占用时,其他线程加写锁时会阻塞,加读锁会成功 2. 策略 读写锁有两种策略:强读同步:读锁优先,只要写锁没有占用那么就可以加读锁强写同步:写锁优先,只能等到所有正在等待或者执行的写锁执行完成后才能加读锁 大部分读写锁的实现都采用的是“强写同步”策略,对尝试加锁的操作进行排队,如果前面已经有尝试加写被锁阻塞住的话,后续加读锁也都会被阻塞住(尽管当前时刻是读锁占用的状态)。这样做的目的主要是为了避免“写饥饿”,在“多读少写”的情况下防止数据修改延迟过高。 当然,具体采取哪种策略取决于业务。例如航班订票系统使用强写同步策略,图书馆查阅系统使用强读同步策略。 3. 应用 非常适用于对数据结构读的次数远大于写的情况,因为读锁是共享的,这样可以提高并行性。 4. 主要接口 5. 例子 条件变量 并发有互斥和等待两大需求,前者是因为线程间存在共享数据依赖而后者是线程间存在依赖,条件变量正是为了解决等待需求。 1. 简介 条件变量本质上也是一个多线程间共享的全局变量,它的功能是阻塞线程,被阻塞的线程直到接收到“条件成立”的信号后才能继续执行。 需要注意的是:条件变量并不是锁(但它几乎总是和互斥量一起使用的),而是线程间的一种通讯机制条件变量本身也不包含条件,它被称为条件变量是因为它经常和条件语句(if/while)一起使用 2. 背景:轮询模式与事件模式 以生产者消费者模型为例,消费者依赖生产者push素进队列才能开始工作。在不考虑条件变量的情况下,一种实现方式是让消费者线程一直轮询队列(需要加mutex锁),判断队列里是否存在素:队列非空:消费队列素队列为空:继续轮询队列(spin策略,浪费CPU性能)或者休眠一段时间后再轮询(sleep策略,浪费该线程性能) 在使用mutex的情况下,队列为空时两种策略都属于轮询(poll)模式。但是有了条件变量后就可以使用事件(event)模式:当消费者线程发现队列为空时就通知操作系统它要wait,待生产者线程push素进队列后就调用signal让操作系统唤醒消费者线程。总结一下:mutex + spin策略(轮询模式):浪费CPU性能,队列为空时消费线程会一直占用CPU空跑mutex + sleep策略(轮询模式):sleep时间过短时存在多次上下文切换开销,sleep时间较长时浪费消费线程性能mutex + 条件变量(事件模式):只有当生产者push素进入队列才会唤醒消费线程,但依然存在上下文切换的开销 因此条件变量的产生正是为了不循环加锁解锁,又可以在第一时间收到条件满足的通知。 3. 实现原理 假设线程A依赖线程B某个条件:线程Amutex访问共享区域,判断条件是否满足如果条件不满足,则调用wait方法等待条件达成线程B准备好条件后通过发singal唤醒线程A
4. 主要接口 5. 例子 仍然以消费者生产者模型为例,在这个场景下消费者和生产者两者是互相制约的:缓冲区为空时:消费者不能消费,只能等待生产者往缓冲区中push素缓冲区满了:生产者不能生产,只能等待消费者从缓冲区中pop素 信号量 1. 简介 信号量分为有名信号量和无名信号量,无名信号量用于线程同步,有名信号量一般用于进程之间管理。 信号量本质上是一个非负的整数计数器,用于控制公共资源的访问,也被称为PV原子操作:P操作:即信号量sem减一,若sem小于等于0则P操作被阻塞,直到sem变量大于0为止V操作:即信号量sem加一 2. 与互斥量的区别 信号量允许多个线程同时进入临界区,而互斥量只允许一个线程进入临界区。 3. 主要接口 Reference [1] https://zhuanlan.zhihu.com/p/372006232 [2] https://zhuanlan.zhihu.com/p/107196194 [3] https://zhuanlan.zhihu.com/p/190199754 [4] https://www.cnblogs.com/caidi/p/11310274.html [5] https://blog.csdn.net/guotianqing/article/details/80559865 [6] https://www.zhihu.com/question/66733477/answer/1267625567 [7] https://www.cnblogs.com/yinbiao/p/11190336.html [8] http://c.biancheng.net/view/8633.html [9] https://blog.csdn.net/sigusoft_39736982/article/details/82380689
2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/32667.html