1.临界区(Critical Section)
我们需要某种手段(互斥)确保当一个进程在使用一个共享变量或文件时,其他进程不能做同样的事情。或者描述为一个进程的一部分时间做内部计算或一些不会引发竞争条件的操作。
我们把对共享内存进行访问的程序片段成为临界区。
2.忙等待互斥
- 屏蔽中断
单处理器系统中,最简单的方法是使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在离开之前打开中断。
这种方法并不好,1.将屏蔽中断权限交给用户可能使系统中止;2.如果系统是多处理器,屏蔽中断只能对单个CPU有效,其他CPU仍然可以访问共享内存。 - 锁变量
软件解决方案,增加一个共享变量(初始值为0),线程每次进入共享先检查共享变量,如果共享变量为0则将其设置为1并进入临界区,离开时将其复位。
并不能解决问题。 - 严格轮换法
下面是这种方法的实现方法://进程0 while (TRUE) { while (turn != 0) //循环等待 critical_section(); turn = 1; noncritical_section(); } //进程1 while (TRUE) { while (turn != 1) //循环等待 critical_section(); turn = 0; noncritical_section(); }
- Peterson解法
#define FALSE 0 #define TRUE 1 #define N 2 //进程数量 int turn; //现在轮到谁 int interested[N]; //所有值初始化为0(FALSE) void enter_section(int process) { //用进程号作为参数 int other = 1-process; //另一方进程 interested[process] = TRUE; turn = process; while (turn == process && interested[other] == TRUE); //空等待 } void leave_section(int process) { interested[process] = FALSE; }
- TSL指令
该方案需要硬件支持,计算机中通常会有以下指令:
TSL RX, LOCK 称为测试并加锁(Test and Set Lock),它将一个内存字lock读到寄存器RX中,然后在该地址上存一个非零值,整个操作是原子性的。
以下是该方案的汇编语言子程序:enter_section: TSL REGISTER, LOCK | 复制锁到寄存器并将锁设置为1 CMP REGISTER, #0 | 锁是0吗? JNE enter_section |若不是0,说明锁已经被设置,循环 RET | 返回调用者,进入临界区 leave_section: MOVE LOCK, #0 | 在锁中存入0 RET | 返回调用者
3.睡眠与唤醒
忙等待方法浪费了CPU时间,而且可能造成优先级反转的问题。现在转换一种思路,当进程无法进入临界区时将其阻塞,而不是忙等待。最简单的是使用sleep和wakeup。sleep是一个将引起调用进程阻塞的系统调用,即被挂起,知道另外一个进程将其唤醒。wakeup调用有一个参数,即要被唤醒的进程。下面是使用sleep和wakeup解决生产者和消费者问题:
#define N 100 int count = 0; void producer(void) { int item; while (TRUE) { item = produce_item(); if (count == N) sleep(); insert_item (item); count ++; if (count == 1) wakeup(consumer); } } void consumer(void) { int item; while (TRUE) { if (count == 0) sleep(); item = remove_item(); count--; if(count == N-1) wakeup(producer); consume_item(item); } }
事实上,这种做法并不完全正确,考虑这种情况:缓冲区为空,消费者刚刚读到count的值发现它为0,此时调度程序暂停消费者并启动生产者。生产者向缓冲区加入一个数据项,count加1。这时生产者会发一个wakeup信号给消费者。但是这是消费者还没有sleep,所以该wakeup信号会丢失。之后生产者总会填满缓冲区,然后sleep。这样一来,两个线程都将永久睡眠下去。
4.信号量
定义一种新的变量类型,称为信号量。一个信号量取值可以为0(表示没有保存下来的唤醒操作)或者正值(表示有一个或多个唤醒操作)。
另外有两种系统调用用于操作信号量:up和down。在信号量上执行down操作时,如果该信号量大于0,则将其减1;如果该信号量为0,则进程将睡眠。执行up操作时,对信号量加1。如果一个或多个线程在该信号量上睡眠,则由系统选择其中一个线程完成它的down操作。这两种操作都是原子操作。下面是用信号量解决生产者消费者问题:
#define N 100 typedef int semaphore semaphore mutex = 1; //控制对临界区的访问 semaphore empty = N; //缓冲区空槽数目 semaphore full = 0; //缓冲区满槽数目 void producer (void) { int item; while (TRUE) { item = produce_item(); down (&empty); down (&mutex); insert_item(item); up (&mutex); up (&full); } } void consumer (void) { int item; while (TRUE) { down (&full); down (&mutex); item = remove_item(); up (&mutex); up (&empty); consume_item(item); } }
从上例中可以看出,信号量mutex被用于实现互斥,而信号量full和empty则用来实现同步。
5.互斥量
互斥量是信号量的简化版本,只有两个状态:加锁和解锁,不具有信号量的计数能力。
Pthread提供了很多可以用来同步线程的函数:
- 与互斥量相关的主要函数
线程调用 描述 pthread_mutex_init 创建一个互斥量 pthread_mutex_destroy 撤销一个互斥量 pthread_mutex_lock 获得一个锁或阻塞 pthread_mutex_trylock 获得一个锁或失败 pthread_mutex_unlock 释放一个锁 - 与条件变量相关的主要函数,条件变量允许线程由于一些未达到的条件而阻塞
线程调用 描述 pthread_cond_init 创建一个条件变量 pthread_cond_destroy 撤销一个条件变量 pthread_cond_wait 阻塞以等待一个信号 pthread_cond_signal 向另一个线程发信号来唤醒它 pthread_cond_broadcast 向多个线程发信号来将它们全部唤醒