进程与线程(下)
1. 消息队列
- 消息队列的通信模式可以解决通信效率低的问题
- 消息队列是保存在内核中的消息链表
- 如果进程从消息队列中读取了消息体,内核就会把这个消息体删除
- 不足
通信不及时
附件也有⼤⼩限制
消息队列通信过程中,存在⽤户态与内核态之间的数据拷⻉开销
2. 共享内存
- 共享内存没有消息的拷贝
- 机制:拿出⼀块虚拟地址空间来,映射到相同的物理内存中
- 需要结合信号量来实现修改共享数据
3. 信号量
- 信号量就是实现了进程间共享内存的保护机制
- 信号量其实是⼀个整型的计数器,主要⽤于实现进程间的互斥与同步,⽽不是⽤于缓存进程间通信的数据
- 信号量初始值为1,代表互斥信号量
- 信号初始化为 0 ,就代表着是同步信号量
4. 信号
- 异常情况下的⼯作模式,就需要⽤「信号」的⽅式来通知进程
- Linux 操作系统中, 为了响应各种各样的事件,提供了⼏⼗种信号
- Ctrl+C 产⽣ SIGINT 信号,表示终⽌该进程
- Ctrl+Z 产⽣ SIGTSTP 信号,表示停⽌该进程,但还未结束
- 信号是进程间通信机制中唯⼀的异步通信机制
执行默认操作
捕捉信号
忽略信号
5. Socket
- 跨⽹络与不同主机上的进程之间通信,就需要 Socket 通信了
- Socket 通信不仅可以跨⽹络与不同主机的进程间通信,还可以在同主机上进程间通信
- Socket系统调用可指定网络/本地;TCP/UDP协议实现不同类型通信
- 针对 TCP 协议通信的 Socket 编程模型
服务端和客户端初始化 Socket ,得到⽂件描述符
服务端调⽤ bind ,将绑定在 IP 地址和端⼝
服务端调⽤ listen ,进⾏监听
服务端调⽤ accept ,等待客户端连接
客户端调⽤ connect ,向服务器端的地址和端⼝发起连接请求
服务端 accept 返回⽤于传输的 socket 的⽂件描述符
客户端调⽤ write 写⼊数据
服务端调⽤ read 读取数据
客户端断开连接时,会调⽤ close ,那么服务端 read 读取数据的时候,就会读取到了 EOF ,待处理完数据后,服务端调⽤ close ,表示连接关闭
- 服务端调⽤ accept 时,连接成功了会返回⼀个已完成连接的 socket,后续⽤来传输数据
- UDP 是没有连接的,所以不需要三次握⼿,也就不需要像 TCP 调⽤ listen 和 connect,但是 UDP 的交互仍然需要 IP 地址和端⼝号,因此也需要 bind
不需要要维护连接,那么也就没有所谓的发送⽅和接收⽅
甚⾄都不存在客户端和服务端的概念
- 本地字节流 Socket 和 本地数据报 Socket 在 bind 的时候,不像 TCP 和 UDP 要绑定 IP 地址和端⼝,⽽是绑定⼀个本地⽂件,这也就是它们之间的最⼤区别
6. 多线程同步
- 互斥
临界区:多线程执⾏操作共享变量的这段代码称为临界区
互斥:保证⼀个线程在临界区执⾏时,其他线程应该被阻⽌进⼊临界区
- 同步
并发进程/线程在⼀些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步
- 同步与互斥是两种不同的概念
同步就好⽐:「操作 A 应在操作 B 之前执⾏」,「操作 C 必须在操作 A 和操作 B 都完成之后才能执⾏」
互斥就好⽐:「操作 A 和操作 B 不能在同⼀时刻执⾏」
7. 同步和互斥的实现和使用
- 锁:加锁、解锁操作
「忙等待锁」
当获取不到锁时,线程就会⼀直 wile 循环,不做任何事情,所以就被称为「忙等待锁」,也称为⾃旋锁(spin lock)
「⽆忙等待锁」
没取到锁,不用自旋,放到锁的等待队列,然后把CPU让给其他程序执行
- 信号量: P、 V 操作
信号量表示资源的数量
PV操作函数是原子性的
信号量不仅可以实现临界区的互斥访问控制,还可以线程间的事件同步
互斥即初始值为1
同步即初始值为0
8. 死锁
- 对多个资源使用不同顺序的互斥锁造成死锁
- 死锁产生条件
互斥条件
指加锁的资源同一时间只能一个线程访问
持有并等待条件
线程 A 在等待资源 2 的同时并不会释放⾃⼰已经持有的资源 1
线程B在等待资源1的同时不会释放自己已经持有的资源1
不可剥夺条件
⾃⼰使⽤完之前不能被其他线程获取
循环等待
循环中的每个进程都在等待下一个进程释放的资源
- 如何破坏死锁:破坏其中⼀个条件即可,最常⽤的⽅法就是使⽤资源有序分配法来破坏环路等待条件
破坏互斥条件
破坏保持等待的条件
破坏不可抢占条件
破坏循环等待条件
9. 锁
- 最底层的两种是「互斥锁和⾃旋锁」,很多⾼级的锁都是基于此实现
10. 互斥锁
- 互斥锁加锁失败后,线程会释放 CPU ,给其他线程
- 互斥锁是⼀种「独占锁」
- 加锁就会失败,就会释放 CPU 让给其他线程, 加锁的代码就会被阻塞
- 对于互斥锁加锁失败⽽阻塞的现象,是由操作系统内核实现的
- 互斥锁加锁失败时,从⽤户态陷⼊到内核态,让内核唤醒,虽然简化了使⽤锁的难度,但存在⼀定的性能开销成本
成本:两次线程上下⽂切换的成本(切换私有数据和寄存器)
- 互斥锁选择:当代码很短(即锁住的时间很短)不应该用互斥锁。因为线程上下文切换反而会更耗时,应选择自旋锁
11. ⾃旋锁
- ⾃旋锁加锁失败后,线程会忙等待,直到它拿到锁
- 通过 CPU 提供的 CAS 函数(Compare And Swap)(原子指令)
使⽤⾃旋锁的时候,加锁失败的线程会「忙等待」,直到它拿到锁。「忙等待」可以⽤ while 循环等待实现,不过最好使⽤ CPU 提供的 PAUSE 指令来实现,因为可以减少循环等待时的耗电量
需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断⼀个线程,运⾏其他线程)。否则,⾃旋锁在单 CPU 上⽆法使⽤,因为⼀个⾃旋的线程永远不会放弃 CPU
- 如果被锁住的代码执⾏时间过⻓,不适合用自旋锁
12. 读写锁
- 读写锁适⽤于能明确区分读操作和写操作的场景
- 没有写锁时,多个线程可以并发持有读锁,可提高共享资源访问你效率
- 有线程持有写锁,其他读、写线程都会被阻塞
- 读写锁在读多写少的场景,能发挥出优势
- 读写锁可以分为「读优先锁」和「写优先锁」以及「读写公平锁」
- 读优先锁
即在读锁占有的情况下,写锁被阻塞,但是其他读锁可以继续持有读锁
缺点:可能造成线程极饿现象
- 写优先锁
写线程不会被饿死;但是读线程仍然有可能被饿死
- 公平读写锁
⽤队列把获取锁的线程排队,不管是写线程还是读线程都按照先进先出的原则加锁即可
13. 乐/悲观锁
- 前面提到的互斥锁、⾃旋锁、读写锁,都属于悲观锁
- 悲观锁
认为多线程同时修改资源概率高,于是很容易出现冲突,所以访问共享资源前,先要上锁
- 乐观锁
反之修改概率较低,用乐观锁
先修改完共享资源,再验证这段时间内有没有发⽣冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作
乐观锁全程并没有加锁,所以它也叫⽆锁编程
涵盖各大厂考官最爱问知识点,22年最新整理!