先决条件 –进程同步
锁变量为进程提供了最简单的同步机制。关于锁定变量的一些值得注意的点是-
- 它是一种在用户模式下实现的软件机制,即不需要操作系统的支持。
- 它是一个繁忙的等待解决方案(即使在技术上等待时也让 CPU 保持忙碌)。
- 它可以用于两个以上的过程。
当 Lock = 0 表示临界区是空的(初始值)而 Lock = 1 表示临界区被占用时。
伪代码看起来像这样——
Entry section - while(lock != 0);
Lock = 1;
//critical section
Exit section - Lock = 0;
在以下代码片段中可以看到更正式的锁定变量方法用于进程同步:
char buffer[SIZE];
int count = 0,
start = 0,
end = 0;
struct lock l;
// initialize lock variable
lock_init(&l);
void put(char c)
{
// entry section
lock_acquire(&l);
// critical section begins
while (count == SIZE) {
lock_release(&l);
lock_acquire(&l);
}
count++;
buffer[start] = c;
start++;
if (start == SIZE) {
start = 0;
}
// critical section ends
// exit section
lock_release(&l);
}
char get()
{
char c;
// entry section
lock_acquire(&l);
// critical section begins
while (count == 0) {
lock_release(&l);
lock_acquire(&l);
}
count--;
c = buffer[end];
end++;
if (end == SIZE) {
end = 0;
}
// critical section ends
// exit section
lock_release(&l);
return c;
}
在这里我们可以看到读写器问题的经典实现。这里的缓冲区是共享内存,许多进程正在尝试读取或写入字符。为了防止任何数据的歧义,我们通过使用锁变量来限制并发访问。我们还对可以访问的读者/作者数量施加了限制。
现在每个同步机制都是根据三个主要参数来判断的:
- 相互排斥。
- 进步。
- 有界等待。
其中互斥是所有参数中最重要的。在某些情况下,锁定变量不提供互斥。通过以如下给出的汇编语言代码的形式编写其伪代码,可以最好地验证这一事实。
1. Load Lock, R0 ; (Store the value of Lock in Register R0.)
2. CMP R0, #0 ; (Compare the value of register R0 with 0.)
3. JNZ Step 1 ; (Jump to step 1 if value of R0 is not 0.)
4. Store #1, Lock ; (Set new value of Lock as 1.)
Enter critical section
5. Store #0, Lock ; (Set the value of lock as 0 again.)
现在让我们假设进程 P1 和 P2 正在竞争临界区,它们的执行顺序如下(Lock 的初始值 = 0)——
- P1 执行语句 1 并被抢占。
- P2 执行语句 1、2、3、4 并进入临界区并被抢占。
- P1 执行语句 2、3、4 并进入临界区。
这里最初进程P1的R0将锁值存储为0,但未能将锁值更新为1。因此,当P2执行时,它也发现LOCK值为0,并通过将LOCK值设置为1而进入临界区。但真正的问题出现了当 P1 再次执行时,它不会检查 Lock 的更新值。它只检查存储在 R0 中的先前值为 0 的值并进入临界区。
这只是许多其他执行顺序中的一种。有些甚至可能提供互斥,但我们不能详述。根据墨菲定律“任何可能出错的事情都会出错”。因此,与所有简单的事情一样,锁定变量同步方法也有其公平的缺点,但它是我们开发更好的同步算法以解决我们在这里面临的问题的一个很好的起点。