1.并发
如果逻辑控制流在时间上是重叠,那么它们就是并发的,操作系统提供三种基本的构造并发程序的方法:进程、I/O多路复用、线程
进程
- 每个逻辑控制流 都是一个进程,由内核来调度和维护
- 因为进程有独立的虚拟地址空间,和其他进程通信,控制流必须使用某种显式的进程间通信IPC进制
基于进程的并发编程,对于在父,子进程间共享状态信息,进程有一个非常清晰的模型
- 共享文件表,但是不共享用户地址空间
- 进程拥有独立的虚拟地址空间
- 优点:一个进程不可能不小心覆盖另一个进程的虚拟存储空间,消除许多令人迷惑的错误
- 缺点:独立的地址空间使得进程间共享信息也很困难,必须使用显式的IPC(进程间通信)机制,速度比较慢,进程控制和IPC的开销都很大
I/O多路复用
- 应用程序在一个进程的上下文中显示地调度它们自己的逻辑流
- 逻辑流被模型化为状态机,数据到达文件描述符后,主程序显式地从一个状态转换到另一个状态
- 因为程序是一个单独的进程,所以所有的流都共享同一个地址空间
- 使用select函数,要求内核挂起进程,只有一个或多个I/O事件发生后,才将控制返回给应用程序
线程
- 线程是运行在一个单一进程上下文中的逻辑流,由内核调度
- 像进程一样由内核进行调度
- 而且像I/O多路复用一流一样共享一个虚拟地址空间
-
线程就是运行在进程上下文中的逻辑流,由内核调度,每个线程都有它自己的线程上下文。
-
不共享
- 包括一个唯一的整数线程ID(Thread ID,TID).
- 栈和栈指针
- 程序计数器
- 通用目的寄存器和条件码
- 共享
- 所有运行在该进程里的线程共享该进程的整个虚拟地址空间
- 共享 包括代码,数据,堆,共享库和打开的文件
- 与进程执行时区别
- 线程的上下文切换的开销比进程的小得多,快得多
- 线程不是按照严格的父子层次来组织
- 和一个进程相关的线程组成一个线程池
2.线程同步
共享变量
共享变量十分方便,但是他们也引入了同步错误的可能性。同一个程序每次运行的顺序都可能不同,这些顺序中有一些将会产生正确结果,但是其他的不会,这就是同步错误
信号量
具有非负整数值的全局变量,只能由两种特殊的操作来处理,这两种操作称为P和V
- P(s),Proberen,测试
- 如果s是非零的,那么P操作将s减1,并且立即返回
- 如果s为零,那么就挂起这个线程,直到s变为非零
- 而一个V操作会重启这个线程
- 在重启之后,P操作将s减1,并将控制返回给调用者
- V(s),Verhogen,增加
- V操作将s加1.
- 如果有任何线程阻塞在P操作等待s变成非零,那么V操作随机会重启这些线程中的一个。
- 然后将s减去1,完成它的P操作。
- P操作和V操作都是不可分割的,也就是自身确保了是一个带有安全轨迹的操作
- P和V的定义确保了一个正在运行的程序绝不可能进入这样一种状态,也就是不可能有负值,这个属性叫做信号量不变性
- 将每个共享变量(或一组相关的共享变量) 与一个信号量s联系起来,然后用P(s)和V(s)操作相应的临界区包围起来
3.利用信号量
互斥锁
- 提供互斥为目的的二元信号量常常也称为互斥锁(mutex)
- 在一个互斥锁上执行P操作叫做互斥锁加锁
- 在一个互斥锁上执行V操作叫做互斥锁解锁
- 对一个互斥锁加了锁还没有解锁的线程称为占用这个互斥锁
生产者-消费者 问题
- 生产者线程反复地生成新的项目,并把它们插入到缓冲区中
- 消费者线程不断地从缓冲区取出这些项目,然后消费使用它们
- 插入和取出项目都涉及更新共享变量
- 必须保证对缓冲区的访问是互斥的
- 还需要调度对缓冲区的访问
- 如果缓冲区是满的,那么生产者必须等待直到有一个槽位变为可用
- 如果缓冲区是空的,那么消费者必须等待知道有一个项目变为可用
读者-写者 问题
- 一组并发的线程要访问同一个数据对象
- 修改对象的线程叫做写者
- 只读对象的线程叫做读者
- 写者必须拥有对对象的独占访问
- 读者可以和无限多个其他读者共享对象
- 一般来说有无数个并发的读者和写者
第一类读者-写者问题
-
读者优先,要求不让读者等待,除非已经把一个使用权限赋予了一个写者。换句话说,读者不会因为有一个写者在等待而等待
-
信号量w控制对访问共享对象的临街区的访问
-
读者
- w只对第一个读者上锁
- w对最后一个走的读者解锁
- 写者
- 写者只要进入临界区就对w上锁
- 写者只要离开临界区就对w解锁
第二类读者-写者问题
- 写者优先,要求一但一个写者准备好可以写,它就会尽可能地完成它的写操作。同第一类不同,在一个写者到达后的读者必须等待,即使这个写者也是在等待
4.线程安全
四种线程不安全函数类
- 不保护共享变量的函数
- 解决方案,利用P,V这样的同步操作来保护共享的变量
- 保持跨越多个调用状态的函数,因为产生的结果依赖于上一个next的值
- 解决方案: 重写
- 使得它不能依赖static,而是依靠调用者在参数中传递状态信息
- 缺点: 需要在曾经成千上百个不同的调用位置,修改,十分麻烦
- 返回指向静态变量的指针的函数,因为一个正在被一个线程使用的变量,可能偷偷被另一个线程悄悄覆盖
- 解放方案:重写函数:让调用者传递存放的结果的指针
- 加锁-拷贝技术:在一个调用位置,互斥锁加锁。调用线程不安全函数,将函数返回的结构拷贝到一个私有的存储器。然后互斥锁,解锁。
- 调用线程不安全函数的函数
5.死锁
-
指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进
-
产生死锁的必要条件
- 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用
- 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放
- 环路等待条件:在发生死锁时,必然存在一个进程–资源的环形链
解决死锁的基本方法
- 预防死锁
- 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
- 只要有一个资源得不到分配,也给这个进程不分配其他的资源:(破坏请保持条件)
- 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
- 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
- 避免死锁
- 银行家算法
- 首先需要定义状态和安全状态的概念。系统的状态是当前给进程分配的资源情况
- 因此,状态包含两个向量Resource(系统中每种资源的总量)和Available(未分配给进程的每种资源的总量)及两个矩阵Claim(表示进程对资源的需求)和Allocation(表示当前分配给进程的资源)
- 安全状态是指至少有一个资源分配序列不会导致死锁。当进程请求一组资源时,假设同意该请求,从而改变了系统的状态,然后确定其结果是否还处于安全状态
- 如果是,同意这个请求;如果不是,阻塞该进程直到同意该请求后系统状态仍然是安全的
- 检测死锁
- 首先为每个进程和每个资源指定一个唯一的号码
- 然后建立资源分配表和进程等待表
- 解除死锁
- 剥夺资源:从其它进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
- 撤消进程:可以直接撤消死锁进程或撤消代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等