程序执行副本
一个程序执行一定存在需要计算资源、存储资源,那么多个程序一起运行时,一定存在如何分配计算机有限的资源,那么这个过程中,每个程序就是一个进程。当有些进程运行中时,会进行图形渲染、请求网络、响应用户操作,需要在进程中并行这些任务(不阻塞),为了不给计算机增加负担,会用更轻量化的进程去做(即线程)。
设计思想
接下来我们思考几个核心的设计约束:
- 进程和线程在内存中如何表示?需要哪些字段?
- 描述信息:这部分是描述进程的唯一识别号,也就是 PID,包括进程的名称、所属的用户等。
- 资源信息:这部分用于记录进程拥有的资源,比如进程和虚拟内存如何映射、拥有哪些文件、在使用哪些I/O 设备等,当然 I/O 设备也是文件。
- 内存布局:操作系统也约定了进程如何使用内存。描述了一个进程大致内存分成几个区域,以及每个区域用来做什么。 每个区域我们叫作一个段。
- 进程代表的是一个个应用,需要彼此隔离,这个隔离方案如何设计?
- 地址空间隔离(即物理隔离)
- 内存空间隔离
- 操作系统调度线程,线程间不断切换,这种情况如何实现?
- 注意中断帮助寄存器保存之前进程 / 线程的状态
- 需要支持多 CPU 核心的环境,针对这种情况如何设计?
- 进程(线程)创建后,就开始排队,此时它会处在“就绪”(Ready)状态
- 当轮到该进程(线程)执行时,会变成“运行”(Running)状态
- 当一个进程(线程)在等待其他操作时,会变成“阻塞”(Block)状态
调度执行
- 非抢占:先到先服务,保证公平性和吞吐量
- 抢占:考虑优先级、最短任务优先(做分级队列)
举例:Linux的O(n),O(1),CFS调度器,使用的策略有,静态动态优先级,时间片分配,全局/每个CPU/每个优先级分配一个队列,CPU任务窃取,虚拟时间,链表,红黑树
进程间通信
- 管道
- 共享内存
- 消息队列
-
锁 & 信号量
竞争
指多个线程对一个资源(内存地址)的读写产生竞争,在这个情况下,输出的值不可预测,取决于竞争时(临界区)的操作顺序,解决办法就是避免同时进入临界区(互斥)
锁
抢占临界区,通过wait - notify实现竞争时的操作顺序,如果一个线程拿到锁,就继续执行,如果竞争失败,就调用wait方法将线程假如等待队列,当线程执行完毕,调用notify通知等待队列中的线程。
信号量
饥饿 & 死锁
线程1获得锁1,等待锁2;线程2获得锁2,等待锁1;于是两个线程都陷入了等待(饥饿),且等待永远不会结束,所以造成死锁需要满足以下4个基本条件
资源抢占存在互斥逻辑
- 持有等待
- 禁止抢占(如果拿不到资源一直会处于等待状态,而不会释放已经拥有的资源)
- 循环等待
解决办法:等待过程设计并发控制算法或超时 / 失败后协商转让
分布式锁
考虑多个服务容器,当多个容器同时调用修改用户积分服务时,一定会发生竞争,以操作Redis为例:
- 从Redis读用户积分
- 修改积分
- 更新Redis
以上都属于原子操作(操作不可再分),由CPU指令提供,如cas、tas指令
这里就一定要加锁,有兴趣的可以了解一下Redis中setnx指令、Zookeeper节点操作等等
悲观锁 / 乐观锁
悲观锁:互斥排异,只保留最后一次更新(如MySQL的表锁、行锁、Java中的锁)
乐观锁:允许不同,先更新的先采纳,后更新的解决冲突(Git版本控制)
去中心化:区块链
不走中心化平台系统处理,不产生竞争,点对点交易,解决信用问题