在这一节中线程和进程的调度其实本质是一样的,所以线程进程看做一个东西,都是调度的单位。
原子操作
所谓原子操作(Atomic Operation)是指不会被调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 Context switch。
PS:有时候甚至连条单条机器指令都不是原子的
由于在线程调度过程中,由于进行了上下文切换,所以程序中如果需要调用全局变量,则在对全局变量的操作的语句,在进行编译之后会拆分成数条机器代码(原子操作),在调度过程中会出现问题,比如以下代码:
new_pid = next_pid++;
其中next_pid为全局变量,将其编译成汇编代码得到:
load next_pid r1
store r1 new_pid
inc r1
store r1 next_pid
若正常两线程执行该程序,输入next_pid = 100,输出的结果应该是:next_pid = 102, new_pid(1) = 100,new_pid(2) = 101。
但是如果在以下位置进行了上下文切换:
执行步骤:
总结:上下文切换导致数据错误,并且错误是随机发生的。
由于上述产生的异常现象(称之为竞态条件Race Condition),这就是为什么要引入同步互斥这些机制的原因,就是要解决这种不确定性的问题。
互斥(Mutual exclusion)是当一个进程处于临界区并访问共享资源时,没有其他进程会处于临界区并且访问任何相同的共享资源。
可以通过锁(Lock)来完成互斥操作
PS:这些锁操作为原子操作。
实现方法:
PS:在同步中互斥还可以通过信号量和管道实现。
临界区(Critical section):是指进程中的一段需要访问共享资源并且当另一个进程处于相应代码区域时便不会被执行的代码区域。简单来说,就是访问共享资源的那段代码就是临界区。
死锁(Dead lock):为两个或以上的进程,在互相等待完成特定任务,而最终没法将自身任务进行下去。
饥饿(Starvation):一个可执行的进程,被调度器持续忽略,以至于虽然处于可执行状态却不被执行,这种状态为饥饿。
PS:和无忙等待对应的是忙等待(busy-waiting),表示的是程序在进行死循环大量占用CPU资源。可以利用休眠对其改善。
同步概念:
合作的并发进程需要按先后次序执行。例如:一个进程的执行依赖于合作进程的消息或信号。当一个进程没有得到来自合作进程的消息或信号时需阻塞等待,直到消息或信号到达才唤醒。
举个栗子:
有临界缓冲区的生产者-消费者问题
其中有三个约束:
同步操作就是要解决这三方面的约束。
设计过程中考虑问题:
基于硬件的原子操作,就是使用锁来编写临界区,很简单:
lock_next_pid -> Acquire();
...
Critical Section;
...
lock_next_pid -> Release();
实现Acquire()和Release()有两种方式:
核心函数TestAndSet():
Boolean TestAndSet(Boolean *target){
Boolean rv = *target;
*target = TRUE;
return rv;
}
特点:
实现(无忙等待)
class Lock{
int value = 0;
WaitQueue q;
};
Lock::Acquire(){
while(TestAndSet(value)){
add one thread t1 to q; // 本次执行的线程 添加至等待队列
schedule();
}
}
Lock::Release(){
value = 0;
remove one thread t from q; // 下一个执行的线程 移出等待队列
wakeup(t);
}
请求分两种情况:
(1)若请求时无正在执行的线程,则value = 0;由于返回值为0,while循环会退出,与此同时value = 1,排斥其他线程进入临界区;接着执行临界区命令。
(2)若请求时有正在执行的线程,则value = 1;由于返回值为1,进入while循环,但此时value值不变;进入到循环后,将当前线程加入至等待队列q中,并且利用schedule()函数对该线程进行休眠,并且当前CPU会选择到下一个合适的线程去执行。直到前操作一个线程release了Lock,并且唤醒了该线程,才接着执行临界区命令。
核心函数Exchange():
void Exchange(boolean *a, boolean *b){
boolean temp = *a;
*a = *b;
*b = temp;
}
特点: 交换变量值
实现(忙等待)
int lock = 0;
Lock::Acquire(){
int key = 1;
while(key == 1){
Exchange(lock, key);
}
}
Lock::Release(){
lock = 0;
}
和Test-and-Set方式类似,请求分两种情况:
(1)若请求时无正在执行的线程,则lock = 0;交换后key = 0,while循环会退出,与此同时lock = 1,排斥其他线程进入临界区;接着执行临界区命令,结束后释放lock。
(2)若请求时有正在执行的线程,则lock = 1;交换后值不变,进入while循环;进入到循环后,忙等待。直到前操作一个线程release了Lock,结束忙等待,才接着执行临界区命令。
PS:此方法不能加入Test-and-Set方式中的唤醒机制,由于while循环必须得进一次。
信号量(semaphore)为一个抽象的数据类型,可用于互斥+同步,由以下三部分组成:
核心思想:
只要满足同步条件就进入执行临界区,如果此时等待互斥条件继续执行,否则睡眠。
核心函数:
class Semaphore{
Semaphore(int value){sem = value;};
int sem;
WaitQueue q;
}
p(){
if(--sem < 0){
add one thread t1 to q;
schedule();
}
}
V(){
if(++sem <= 0){
remove one thread t from q;
wakeup(t);
}
}
PV操作实现:(PV操作为原子操作)
以消费者和生产者关系为例,进入P()时sem <= 0代表Buffer(没有数据/满数据)则需要等待,此时会将sem小于0。在正在执行的进程释放后,会由于++sem<=0知道有进程正在等待,唤醒进程。
以下使用mutex类名来表示互斥,以condition_name类名来表示同步。
实现互斥的方法和原子操作类似:
mutex = new Semaphore(1);
mutex->P();
...
Critical Sextion;
...
mutex->V();
在信号量实现互斥过程中,sem初值为1;
每一个约束使用一个单独的信号量:
初值设置:
PS:在任意时刻有关系 如下 FullBuffer.sem + EmptyBuffer.sem = n
以同步中举的列子进行分析
class BoundedBuffer{
mutex = new Semaphore(1);
FullBuffer = new Semaphore(0);
EmptyBuffer = new Semaphore(n);
}
BoundedBuffer::Deposit(c){
EmptyBuffer->P();
mutex->P();
Add data c to the buffer;
mutex->V();
FullBuffer->V();
}
BoundedBuffer::Remove(c){
FullBuffer->P();
mutex->P();
Remove data c from the buffer;
mutex->V();
EmptyBuffer->V();
}
分析:
Critical srction 区间为入栈和出栈的操作,所以互斥操作出现在入栈出栈上下,与管程相区别。
管程 (monitor):是包含了一系列的共享变量和以及这些变量的一些操作的函数的组合和模块,由一下数个部分组成:
核心思想:
只要满足互斥条件就进入执行临界区,如果满足同步条件就继续执行,否则睡眠。则Critical srction 区间为整个操作函数(模块化),与信号量相区别。
核心函数:
class Condition{
int NumWaiting = 0; //代表等待中的进程数
WaitQueue q;
};
Condition::Wait(lock){ // 不满足约束进入
NumWaiting++; // 等待进程数+1
Add thread t1 to q;
release(lock); // 释放锁进入休眠
schedule();
require(lock); // 休眠完成得到锁
}
Condition::Signal(){
if(NumWaiting > 0){
Remove thread t from q;
wakeup(t);
NumWaiting--;
}
}
以同步中举的列子进行分析
class BoundedBuffer{
Lock lock;
int count = 0;
Condition notFull, notEmpty;
};
BoundedBuffer::Deposit(c){
lock->Acquire();
while(count == n) // 判满
notFull.Wait(&lock);
Add thread t1 to buffer;
count++;
notEmpty.Signal();
lock->Release();
}
BoundedBuffer::Remove(c){
lock->Acquire();
while(count == 0) // 判空
notEmpty.Wait(&lock);
Remove thread t from buffer;
count--;
notFull.Signal();
lock->Release();
}
使用while的原因:
在hansen-style下,进程在上一个进程的signal()被唤醒后没有马上对其进行执行,(由于后边还有release操作),这个时候可能存在多个被唤醒的进程,所以需要while在被唤醒后再判断自己是否抢到了该进程。
而相对应的Hoare-style下,进程在上一个进程的signal()被唤醒后直接调度回当前进程,不存在有多个进程被唤醒,所以此时while可以改变为if。
其中hansen-style和Hoare-style调度示意图如下所示
问题背景:共享数据的访问,一方写一方读,读的时候不能写,写的时候不能读。则这个时候会产生优先级问题:
优先级:
class W_R{
mutex = new Semaphore(1); // 保证读写操作是互斥的
countmutex = new Semaphore(0); // 保证Rcout的自加自减是互斥的
Rcount = 1; // 用于计算读者数量
}
W_R::Write(){
mutex->P();
write;
mutex->V();
}
W_R::Read(){
countmutex->P();
if(Rcount == 0) // 如果读者为空,判断是否有写者正在执行,与写者互斥
mutex->P(); // 如果有读者,则直接和读者一起阅读
++Rcount;
countmutex->V();
read;
countmutex->P();
--Rcount;
if(Rcount == 0) // 如果读者为空,释放锁给写者
mutex->V(); // 如果读者不为空,不释放
countmutex->V();
}
class W_R{
AR = 0; // active reader
AW = 0; // active writer
WR = 0; // waiting reader
WW = 0; // waiting writer
Condition okToRead;
Condition okToWrite;
Lock lock; // while互斥
};
public W_R::Read(){
StartRead();
read;
DoneRead();
}
public W_R::Write(){
StartWrite();
Write;
DoneWrite();
}
private W_R::StartRead(){
lock.Acquire();
while((AW + WW) > 0){ // 如果有正在写或者正等待的写者,则进入休眠
WR++;
okToRead.wait(&lock);
WR--;
}
AR++;
lock.Release();
}
private W_R::DoneRead(){
lock.Acquire();
AR--;
IF(AR == 0 && WW > 0) // 如果没有正在读的读者并且有等待的写者,则释放锁
okToWrite.signal(&lock);
lock.Release();
}
private W_R::StartWrite(){
lock.Acquire();
while((AW + AR) > 0){ // 如果有正在执行的人,则进行休眠(同时只能一个人在写)
WW++;
okToWrite.wait(&lock);
WW--;
}
AW++;
lock.Release();
}
private W_R::DoneWrite(){
lock.Acquire();
AW--;
if(WW > 0) // 如果此时还有等待的写者,将锁交给下一个写者
okToWrite.signal();
else if(WR > 0) // 如果此时有等待的读者,唤醒所有的读者
okToRead.broadcast(&lock);
lock.Release();
}
问题描述:桌子上游五把叉子,五个哲学家每个人需要左右两把叉子才能吃饭。如图所示
解决方法:
#define N 5 // 哲学家数量
#define LEFT (i-1+N)%N // 右邻居标号
#define RIGHT (i+1)%N // 左邻居标号
#define THINK 0 // 思考状态
#define HUNGRY 1 // 饥饿状态
#define EATING 2 // 进餐状态
int state[N] = {THINK, THINK, THINK, THINK, THINK};// 状态数组
semaphore mutex; // 互斥
semaphore s[N]; // 同步
void philosopher(int i){
while(1){
think(); // 思考
take_forks(i); // 拿起叉子
eat(); // 吃饭
put_forks(i); // 放下筷子
}
}
void take_forks(int i){
mutex.P();
state[i] = HUNGRY; // 饥饿状态
test_take_LR_forks(i); // 试图唤醒自己,如果此次唤醒不了则需要左邻居或者右邻居唤醒
mutex.V();
s[i].P(); // 想进入吃饭,但是没叉子进入休眠状态
}
void put_forks(int i){
mutex.P();
state[i] = THINKING;
test_take_LR_forks(LEFT); // 试图唤醒左邻居
test_take_LR_forks(RIGHT);// 试图唤醒右邻居
mutex.V();
}
void test_take_LR_forks(int i){ // 如果左边右边都有筷子,且处于饥饿状态就可以吃饭
if(state[i] == HUNGRY && state[LEFT] != EATING && state[RIGHT] != EATING){
state[i] = EATING;
s[i].V();
}
}
分析: