进程与线程(下)

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年最新整理!

全部评论

相关推荐

评论
13
63
分享
牛客网
牛客企业服务