多线程vs多进程
多线程/多进程应用场景
当我们的程序中存在重复性较高的工作,我们通常希望多个“单位”同时操作以提高该工作的效率。这个“单位”为进程or线程,那么什么场景下选择进程,什么场景下选择线程呢?
进程和线程,本质上的区别是CPU时间调度上的时间片。
多线程由于不需要进行进程间的上下文切换能够提高进程时间片内CPU的利用效率,但是不会因此占用更多的CPU资源(处理时间片)。
相反,如果同时运行多个进程,每个进程独自占用CPU的资源,尤其对于性能较佳的多核CPU而言,能够大幅提升运行速度(占用更多的时间片资源)。
大部分任务都是IO密集型任务,CPU消耗很少,花费更多时间等待IO操作完成,对于这类任务,瓶颈不在CPU而在IO,没有必要采用多进程占用更多的CPU资源,可以使用多线程。
对于CPU密集型任务,大多数时间都是CPU在运行计算,可以考虑采用多进程占用更多的CPU资源,但需要考虑同时运行的任务数量与CPU核心数相匹配。
线程间同步
线程共享同一资源,虽然线程被CPU作为最小执行单元,但是很少有线程能够在自己的时间片内执行完。当多个线程请求修改同一变量,会导致变量的处理结果不可控。
为了确保同一变量的按序修改,在某一线程修改变量时禁止其他线程对其的任何访问的行为称作线程同步。
1.互斥锁
本质就是一个特殊的全局变量,拥有lock和unlock两种状态,unlock的互斥锁可以由某个线程获得,一旦获得,这个互斥锁会锁上变成lock状态,此后只有该线程由权力打开该锁,其他线程想要获得互斥锁,必须得到互斥锁再次被打开之后。
互斥锁的弊端:被访问的变量代表一个状态,而某个线程需要反复询问该状态是否改变时,会造成线程反复请求变量的权限,反复加锁和解锁,耗费大量资源,效率低。
其实上述情况,请求状态变量的线程只需要在状态改变的时候被唤醒就可以了。另一种同步方式条件变量能够实现这一操作。
2.条件变量
条件变量和互斥锁往往配合使用,例:
(1)线程A获得互斥锁pthread_mutex_lock(&qlock);
(2)线程A等待条件pthread_cond_wait(&qready,&qlock)à该步骤会自动释放(1)中互斥锁;
(3)线程B获得互斥锁pthread_mutex_lock(&qlock);并改变相关变量;
(4)线程B激活条件变量pthread_cond_signal(&qready);
(5)线程B解锁;
(6)线程A自动获得原互斥锁(pthread_cond_wait(&qready,&qlock)干的),并继续运行。
上述两种线程同步方式能够解决在变量存在修改行为时的访问互斥与同步,但是如果变量不需要修改能不能让多个读线程同时访问呢?
3.读写锁
读写锁=共享-独占锁
锁处于读模式时可以线程共享,而锁处于写模式时只能独占。
读写锁有两种策略:强读同步和强写同步。
在强读同步中,总是给读者更高的优先权,只要写者没有进行写操作,读者就可以获得访问权限。à读写锁默认的模式,静态初始化只能默认
在强写同步中,总是给写者更高的优先权,读者只能等到所有正在等待或者执行的写者完成后才能进行读。
上面三种同步方式都是针对某一资源的写互斥,那么如果同时管理多个可访问资源呢?
4.信号量
信号量对访问资源的线程计数,当线程请求资源访问时,信号量>0则授予线程访问权限,并将信号量-1,若信号量=0,则认为资源并发量已满,组织线程访问。
例:10个顾客,2个服务窗口;5辆车,3个车位。
进程间通信
管道只能承载无格式字节流,缓冲区大小受限。
1.管道
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系(共有祖先)的进程间使用。fifo
2.有名管道
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。mkfifo
3.信号
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。(参考线程间同步)
4.信号量
(1)信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。(参考线程间同步)
(2)信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。
(3)每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
(4)支持信号量组。
5.消息队列
消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。
(1)消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级。
(2)消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
(3)消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取。
6.共享内存
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。
(1)共享内存是最快的一种 IPC,因为进程是直接对内存进行存取。
(2)因为多个进程可以同时操作,所以需要进行同步。
(3)信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问。
7.套接字
IP+Port,用于不同设备的进程间通信。socket
死锁
1.死锁的定义
线程存在以下5种状态:新建-New,就绪-Runnable,运行-Running,阻塞-Blocked,死亡-Dead。
(1)新建-New:新创建了一个线程对象。
(2)就绪-Runnable:新建后,其他线程调用了该对象的start()方法。使线程存在于“可运行线程池”中,等待获取CPU的使用权,即除CPU外其他资源全部已获得。
(3)运行-Running:获取了CPU的使用权后,执行相应的计算,由就绪态转为运行态。
(4)阻塞-Blocked:线程因为某种原因放弃CPU的使用权,暂时停止运行。进入阻塞态后,只有再次进入就绪态才有机会转到运行态。
- 等待阻塞:运行的线程执行wait()方法,该线程会释放占用的所有资源,JVM会把该线程放入“等待池”中。进入这个状态后,是不能自动唤醒的,必须依靠其他线程调用notify()或notifyAll()方法才能被唤醒,唤醒后进入“锁池”中,通过获取锁状态来判断是否进入就绪状态。
- 同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入“锁池”中。
- 其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
(5)死亡-Dead:线程执行完毕or异常退出了run()方法,线程生命周期结束。
死锁,是在多线程竞争资源的情况下,两个或两个以上的线程,互相持有对方需要的资源,导致线程一直处于阻塞的情况。
2.死锁的必要条件
- 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
- 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
- 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
- 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的进程集合{Pl, P2, ..., pn},其中Pi等待的资源被P(i+1)占有(i=0, 1, ..., n-1),Pn等待的资源被P0占有。
3.避免死锁——银行家算法
避免死锁的本质是打破死锁的必要条件,其中银行家算法是一个避免死锁的著名算法。
将操作系统看做银行家,操作系统对于资源的管理类似于银行家对于资金的管理,原则如下:
(1)当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;(首次申请资源充足)
(2)顾客可以分期贷款,但贷款的总数不能超过最大需求量;(后续申请资源充足)
(3)当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;(资源使用优先级)
(4)当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金.(资源使用时限,保证资源释放)

浙公网安备 33010602011771号