背景
引入临界段问题,其解决方案可以保证共享数据的一致性
提出了临界截面问题的软硬件解决方案
引入原子事务的概念,并描述确保原子性的机制
对共享数据的并发访问可能导致数据不一致
维护数据一致性需要确保协作过程有序执行的机制
假设我们希望为填满所有缓冲区的消费者-生产者问题提供一个解决方案。
为此,我们可以使用一个整数计数来跟踪满缓冲区的数量。
最初,count设置为0。生产者在产生一个新的缓冲区后递增,消费者在消耗一个缓冲区后递减。
制作人
while (true) {
/* produce an item and put in nextProduced */
while (count == BUFFER_SIZE)
; // do nothing
buffer [in] = nextProduced;
in = (in + 1) % BUFFER_SIZE;
count++;
}
顾客
while (true) {
while (count == 0)
; // do nothing
nextConsumed = buffer[out];
out = (out + 1) % BUFFER_SIZE;
count–;
/* consume the item in nextConsumed
}
竞争条件
Count ++可以实现为
register1 =计数
Register1 = Register1 + 1
数= register1
Count—可以被执行为
register2 =计数
Register2 = Register2 - 1
数= register2
将这个执行与“count = 5”交织在一起:
S0: producer execute register1 = count {register1 = 5}
S1:生产者执行register1 = register1 + 1 {register1 = 6}
S2: consumer execute register2 = count {register2 = 5}
S3:用户执行register2 = register2 - 1 {register2 = 4}
S4: producer execute count = register1 {count = 6}
S5: consumer execute count = register2 {count = 4}
关键部分的问题
- 临界区问题
- 一个典型的过程Pi的一般结构
- 临界截面问题的解
-
互斥——如果进程Pi在它的临界区执行,那么其他进程就不能在它们的临界区执行
-
进程——如果没有进程在其关键区段中执行,并且存在一些进程希望进入它们的关键区段,那么将进入下一个关键区段的进程的选择就不能无限期地推迟
-
有限制的等待——在一个进程发出请求进入其临界区之后和该请求被批准之前,其他进程被允许进入其临界区的次数必须存在一个限制
假设每个进程以非零速度执行
没有关于N个过程的相对速度的假设
彼得森的解决方案
两个流程解决方案
假设LOAD和STORE指令是原子指令;也就是说,不能被打断。
这两个过程共享两个变量:
int转;
布尔标志[2]
可变转数表示进入临界段的转数。
标志数组用于指示进程是否准备好进入临界区。flag[i] = true表示进程Pi已经准备好了!
- 过程Pi算法
do {
flag[i] = TRUE;
turn = j;
while (flag[j] && turn == j);
critical section
flag[i] = FALSE;
remainder section
} while (TRUE);
- 证明该算法是正确的
- 互斥保持不变
2.满足了进度要求。
3.满足有界等待需求
do {
flag[i] = TRUE;
turn = j;
while (flag[j] && turn == j);
critical section
flag[i] = FALSE;
remainder section
} while (TRUE);
do {
flag[j] = TRUE;
turn = i;
while (flag[i] && turn == i);
critical section
flag[j] = FALSE;
remainder section
} while (TRUE);
do {
flag[i] = TRUE;
turn = j;
while (flag[j] && turn == j);
critical section
flag[i] = FALSE;
remainder section
} while (TRUE);
do {
flag[j] = TRUE;
turn = i;
while (flag[i] && turn == i);
critical section
flag[j] = FALSE;
remainder section
} while (TRUE);
- 3.满足有界等待需求
do {
flag[i] = TRUE;
turn = j;
while (flag[j] && turn == j);
critical section
flag[i] = FALSE;
remainder section
} while (TRUE);
do {
flag[j] = TRUE;
turn = i;
while (flag[i] && turn == i);
critical section
flag[j] = FALSE;
remainder section
} while (TRUE);
同步的硬件
任何解决临界截面问题的方法都需要一个简单的工具——一把锁。
通过要求用锁保护关键区域,可以防止出现竞争条件
do {
acquire lock
critical section
release lock
remainder section
} while (TRUE);
许多系统为关键段代码提供硬件支持
单处理器——可以禁用中断
当前运行的代码将在没有抢占的情况下执行
通常在多处理器系统上效率太低
使用此功能的操作系统不能广泛地扩展
现代机器提供特殊的原子硬件指令
原子= non-interruptable
要么测试记忆字和设定值
或者交换两个记忆词的内容
- TestAndndSet指令
Definition:
boolean TestAndSet (boolean *target)
{
boolean rv = target; / Test */
target = TRUE; / Set */
return rv:
}
- 解决方案使用TestAndSet
Shared boolean variable lock., initialized to false.
Solution (Mutual-Exclusion):
do {
while ( TestAndSet (&lock ))
; // do nothing
// critical section
lock = FALSE;
// remainder section
} while (TRUE);
交换指令
Definition:
void Swap (boolean *a, boolean *b)
{
boolean temp = *a;
*a = *b;
*b = temp:
}
- 解决方案使用交换
Shared Boolean variable lock initialized to FALSE; Each process has a local Boolean variable key
Solution(Mutual-Exclusion):
do {
key = TRUE;
while ( key == TRUE)
Swap (&lock, &key );
// critical section
lock = FALSE;
// remainder section
} while (TRUE);
TestandSet()的有界等待互斥
do {
waiting[i] = TRUE;
key = TRUE;
while (waiting[i] && key)
key = TestAndSet(&lock);
waiting[i] = FALSE;
// critical section
j = (i + 1) % n;
while ((j != i) && !waiting[j]) (Find next waiting process)
j = (j + 1) % n;
if (j == i)
lock = FALSE; (No one is waiting)
else
waiting[j] = FALSE; (process j enters next)
// remainder section
} while (TRUE);
- 证明该算法是正确的
\1. 互斥保持不变
\2. 满足了进度要求。
3.满足有界等待需求
信号量
基于硬件的CS问题解决方案对于应用程序程序员来说是复杂的。
为了克服这个困难,我们使用了一种叫做信号量的同步工具。
信号量S -整数变量
修改S的两个标准操作:wait()和signal()
最初称为P()和V()
那么复杂
只能通过两个不可分割(原子)操作访问
wait (S) {
while S <= 0 /* Semaphore S is occupied /
; // no-op
S–; / Semaphore S is available, get it /
}
signal (S) {
S++; / Release the semaphore S */
}
信号量使用
计数信号量-在一个不受限制的域上的整数范围
二进制信号量-整数值只能在0到1之间;可以更简单地实现吗
也称为互斥锁,因为它们是提供互斥的锁。
我们可以使用二进制信号量来处理多进程的CS问题。
n个进程共享一个初始化为1的互斥信号量
使用信号量的互斥实现
- 提供互斥(Process Pi)
Semaphore mutex; // initialized to 1
do {
wait (mutex);
// Critical Section
signal (mutex);
// remainder section
} while (TRUE);
计数信号量可用于控制对由有限数量实例组成的给定资源的访问。
信号量被初始化为可用资源的数量。
要使用资源,请使用wait()
要释放一个资源,使用signal()
信号量可以用来解决各种同步问题。
例如,我们有两个过程P1和P2。先执行S1,再执行S2: sync = 0
信号量实现
以前的互斥解决方案的主要缺点是等待繁忙(CPU浪费)。
这种类型的信号量称为自旋锁。
为了克服这个问题,我们可以使用block和唤醒操作的概念。
Typedef struc {
int value;
struct process *list;
} semaphore
- 没有繁忙等待的信号量实现
每个信号量都有一个相关的等待队列。等待队列中的每个条目有两个数据项:
值(整数类型)
值> 0表示信号量仍然可用
值= 0表示信号量被占用
没有等待处理
“< 0”表示等待进程数
指向列表中下一条记录的指针/等待列表/
两个操作:
阻塞-将调用该操作的进程放置在适当的等待队列上。
唤醒——删除等待队列中的一个进程,并将其放入就绪队列中。
Implementation of wait
wait(semaphore *S) {
S.value --;
if (S.value < 0) {
add this process to S.list;
block();
}
}
实现的信号
signal(semaphore *S) {
S.value ++;
if (S.value <= 0) {
remove a process P from S.list;
wakeup§;
}
}
- 没有繁忙等待的信号量实现
注意,信号量的值可能是负数。
它的大小是等待该信号量的进程数量。
等待进程列表可以通过每个进程控制块(PCB)中的链接字段轻松实现。
- 死锁和饥饿
死锁——两个或多个进程无限期地等待只由一个等待进程引起的事件
设S和Q是两个初始化为1的信号量
饥饿——无限期的阻塞。一个进程可能永远不会从它被挂起的信号量队列中删除
优先级反转
优先级反转——当低优先级进程持有高优先级进程所需的锁时的调度问题。
三个进程L, M, H,优先级为L < M < H
假设进程H需要资源R,进程l正在使用资源R。进程H等待。
假设进程M变为可运行状态,从而抢占进程L。
间接地,一个较低优先级的进程——M——影响了H必须等待L释放R的时间。
优先级继承协议——所有正在访问高优先级进程所需资源的进程都将继承高优先级进程,直到它们完成资源的使用。