进程间通信:共享资源的互斥访问方法
1、忙等待
1.1 自旋锁:A lock that uses busy waiting
对于自旋锁解决关键区问题的代码:
/*自旋锁的解法*/
//进程0的程序:
while(TRUE){
while(turn != 0); //注意循环被分号终止
criticial_region(); //关键区代码
turn = 1; //关键区结束,改变自旋锁的值,使进程1能够进入
noncriticial_region(); //非关键区代码
}
//进程1的程序:
while(TRUE){
while(turn != 1);
criticial_region(); //关键区代码
turn = 0; //关键区结束,改变自旋锁的值,使进程1能够进入
noncriticial_region(); //非关键区代码
}
对于这种自旋锁的解法,虽然实现了对关键区的互斥访问,但是存在问题:自旋锁的解法只能是两个进程轮换执行的。假如进程1不执行,进程0需要连执行,那么进程0第二次进入关键区将会失败:进程0被一个关键区外的进程阻塞了。
这种做法是不可取的。
1.2 Peterson解法
Peterson的解法则不需要严格轮换的软件互斥。
enter_region中的空语句while循环起到的就是一种忙等待的作用。
#define FALSE 0
#define TRUE 1
#define N 2
int turn;
int interested[N]; //所有的进程都初始化为不感兴趣:0
void enter_region(int process) //进程进入关键区,传入该进程的进程号
{
int other;
other = 1 - process; //另外一个进程的进程号
interested[process] = TRUE; //表示感兴趣
turn = process; //设置标志
while(turn == process && interested[other]==TRUE);//空语句
}
void leave_region(int process)
{
interested[process] = FALSE;
}
一开始没有任何进程在关键区内,进程0调用enter_region
,满足条件,进入关键区。由于1不进入关键区,进程0leave_region
很快返回。假如1此时要进入关键区,那么因为interested[other] == true
他将进入忙等待状态,直到进程0调用leave_region
后才结束忙等待,成功进入关键区。
再考虑两个进程几乎同时调用enter_region
方法,进程1稍后些。那么trun的值将被后进入的进程1覆盖,turn = 1
。那么进程0循环0次进入关键区,进程1只能循环等待0的退出。
1.3 TSL
需要硬件支持。
TSL RX,LOCK
:测试并加锁。将读写保证不可分割,即在指令结束前,其他的处理器不能访问内存字。执行TSL会锁住总线。
锁住总线与屏蔽中断是不同的。
2、睡眠与唤醒
2.1 信号量semaphore
PV操作
信号量解决生产者与消费者问题:
设置三个信号量:full,empty,mutex;
- full:充满的缓冲槽数目。初值为0
- empty:空的缓冲槽数目。初值为缓冲槽总数
- mutex:互斥量,确保生产者和消费者不同时进入关键区。初值为1,表示可进入关键区的名额
#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore full = 0;
semaphore empty = N;
void producer(void)
{
while(TRUE){
item = produce_item(); //先生产一下东西
down(&empty); //先将空槽数目减去1
down(&mutex); //进入关键区
/*************注意11行和12行的代码顺序不能反!!!!******************/
insert_item(); //生产
up(&mutex); //出关键区
up(&full); //满槽数目+1
}
}
void consumer(void)
{
while(TRUE){
down(&full); //满槽数目-1
down(&mutex); //进入关键区
item = remove_item(); //消费
up(&mutex); //出关键区
up(&empty); //空槽数目加1
}
}
这里的11行和12行顺序不能反,不然可能在关键区内阻塞,造成死锁。
这里的full和empty都是信号量,用来解决同步——即事件的顺序发生。
而mutex是互斥量
2.2 条件变量和互斥量的联合使用
条件变量和信号量一样,也是用来解决同步问题的,即解决时序问题。
条件变量condc, condp需要和一个互斥锁结合起来使用。主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。
#include<stdio.h>
#include<pthread>
#define MAX 1000000000 //需要生产的数量
pthread_mutex_t the_mutex; //互斥量数据类型
pthread_cond_t condc, condp; //条件变量类型
int buffer = 0;
void* producer(void* ptr)
{
int i; //生产数据
for(int i = 1; i<=MAX; i++){
pthread_mutex_lock(&the_mutex); //获得一个锁或者阻塞
while(buffer!= 0){
pthread_cond_wait(&condp,&the_mutex); //阻塞以等待一个信号
}
buffer = i; //将数据放入缓冲区
pthread_mutex_signal(&condc);
pthread_mutex_unlock(&the_mutex);
}
pthread_exit(0);
}
void* consumer(void* ptr)
{
int i;
for(int i = 0; i <= MAX; i++){
pthread_mutex_lock(&the_mutex); //互斥使用缓冲区
while(buffer==0){
pthread_cond_wait(&condc, &the_mutex);
}
buffer = 0; //从缓冲区取出数据
pthread_cond_signal(&condp); //唤醒缓冲区
pthread_mutex_unlock(&the_mutex); //释放缓冲区
}
pthread_exit(0);
}
int main(int argc, char** argv)
{
pthread_t pro, con;
pthread_mutex_init(&the_mutex);
pthread_cond_init(&condc,0);
pthread_cond_init(&condp,0);
pthread_create(&con, 0, consumer, 0);
pthread_create(&pro, 0, producer, 0);
pthread_join(pro, 0);
pthread_join(con, 0);
pthread_cond_destory(&condc);
pthread_cond_destory(&condp);
pthread_mutex_destory(&the_mutex);
}
这里使用的pthread_mutex_wait()
与P操作有所不同,在调用pthread_cond_wait()
前必须由本线程加锁(pthread_mutex_lock(&the_mutex)
),而在更新条件等待队列以前,mutex保持锁定状态,并在线程挂起进入等待前解锁。在条件满足从而离开pthread_cond_wait()
之前,mutex将被重新加锁,以与进入pthread_cond_wait()
前的加锁动作对应。阻塞时处于解锁状态。
也正是因为线程在阻塞时处于解锁状态,这个程序也就不会出现在2.1信号量中11和12行换行出现死锁的情况了。
3.管程的使用
信号量的使用需要程序员有较高的素质,管程的使用规避了这些可能出现的人为的错误。
管程:一个由过程,变量,数据结构组成的集合,他们组成一个特殊的模块或者软件包。进程可以在任何需要的时候调用管程内的过程,但他们不能在管程之外声明的过程中直接访问管程内的数据结构。
管程是一个语言概念,C语言不支持管程,下面的程序使用JAVA描述管程的使用。
public class ProducerConsumer{
static final int N = 100; //定义缓冲区大小的常量
static producer p = new producer();
static consumer c = new consumer();
static our_monitor mon = new monitor();
public static void main(String args[]){
p.start();
c.start();
}
static class producer extends Thread{
public void run(){
int item;
while(true){
item = produce_item();
mon.insert(item); //调用管程,加入新生产的物品
}
}
private int_produce_item(){...}//生产的具体代码
}
static class consumer extends Thread{
public void run(){
int item;
while(true){
item = mon.remove();
consume_item(item);
}
}
public void comsume_item(int item){} //消费的具体代码
}
static class our_monitor{
private int buffer[] = new int[N];
private int count = 0, lo = 0, hi = 0;
public synchronized void insert(int val){
if(count == N){
go_to_sleep(); //缓冲区满了,进入睡眠
}
buffer[hi] = val; //插入一个新的数据项
hi = (hi+1)%N; //设置项一个数据项
count = count + 1;
if(count == 1)notify(); //如果消费者在休眠,将其唤醒
}
public synchronized int remove(){
if(count == 0){
go_to_sleep(); //缓冲区满了,进入阻塞
}
val = buffer[lo] //从缓冲区取出一个数据项
lo = (lo+1)%N; //设置项一个数据项
count = count - 1;
if(count == N - 1)notify(); //如果生产者在休眠,将其唤醒
}
private void go_to_sleep(){
try{
wait();
}catch(InteruptedException exc){
}
}
}
}