同步互斥
背景 独立进程:不和其他进程共享资源或状态,具有确定性(输入决定结果);可重现(能够重现起始条件);调度顺序不重要。
并发进程:多个进程之间有资源共享;不确定性;不可重现。某些情况下调度的不一致会造成结果的不一致,也可能出现不可重现性。程序错误也可能是间歇性发生的。
利用原子操作实现一个锁。
Lock.Acquire()
在锁被释放前一直等待,然后获得锁;
如果两个线程都在等待同一个锁,那如果锁被释放了,只有一个进程能得到锁
Lock.Release()
解锁并唤醒任何等待中的进程。
过程:
进入临界区
操作
退出临界区
临界区访问规则:
空闲则入:没有进程在临界区时任何进程可以进入; 忙则等待:有进程在临界区,则其他进程均不能进入临界区; 有限等待:等待进入临界区的进程不能无线等待; 让权等待:不能进入临界区的进程,需要及时释放CPU
基于软件的同步方法
软件方法:两个线程,T0和T1,线程可以通过共享一些共有变量来同步行为。
采用共享变量,设置一个共享变量表示允许进入临界区的线程;
设置一个共享变量数组,描述每个变量是否在临界区中,先判断另一个线程的flag是否是1,如果可以进入了,设置自己的flag;可能会同时等待或同时进入;
Peterson算法:turn表示该哪个进程进入临界区,flag[]表示进程是否准备好进入临界区。在进入区进程i要设置flag[i]=true,且turn=j,判断(flag[i] && turn==j),如果j没有申请进入,则i直接进去没问题。如果j也申请了,看谁先向trun里写数据,谁先写谁进入,由总线仲裁决定先后顺序!
N线程时,采用Eisenberg和McGuire算法,采用一个处理循环。
基于软件的方法很复杂,是一个忙等待
自旋锁
当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。这种采用循环加锁 -> 等待的机制被称为自旋锁(spinlock)
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会导致线程发生两次上下文切换! 但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗,其它需要 cpu 的线程又不能获取到 cpu,造成 cpu 的浪费。所以这种情况下我们要关闭自旋锁。
信号量
多线程的引入导致了资源的竞争,同步是协调多线程对共享数据的访问,在任何时候只能有一个线程执行临界区代码。
信号量是操作系统提供的协调共享资源访问的方法,软件同步是平等线程间的一种同步协商机制。信号量是由OS负责管理的,OS作为管理者,地位高于进程。用信号量表示一类资源,信号量的大小表示资源的可用量。
信号量是一种抽象数据类型,由一个整型变量(共享资源数目)和两个原子操作组成。
PV 操作,P代表sem减一,V 代表sem 加一
信号量是被保护的整型变量,初始化完成后只能通过PV操作修改,是由操作系统保证PV操作是原子操作的。
二进制信号量,资源数目是0或1;资源信号量,资源数目为任意非负值。
第一个进程进来之后,mutex是0了,第二个进程再执行到P操作时,mutex变成-1,则会等待。第一个进程执行结束后,执行V操作,-1变成0,这时候唤醒第二个进程。
必须成对使用P()和V()操作。P()保证互斥访问,V()操作保证使用后及时释放。
一种是条件同步,初值设置为0。事件出现时设置为1。这个事件就相当于是一种资源。
管程
在管程内部使用了条件变量,管程是一种用于多线程互斥访问共享资源的程序结构,采用了面向对象的方法,简化了线程间的同步控制,在任意时刻最多只有一个线程执行管程代码。正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复。
条件变量是管程内的等待机制,进入管程的线程因资源占用而进入等待,每个条件变量表示一种等待原因,对应一个等待队列。两个操作:
-
Wait():将自己阻塞到等待队列中,唤醒一个等待者或释放管程的互斥访问。
-
Signal():将等待队列中的一个线程唤醒;如果等待队列为空,则相当于空操作。