操作系统学习笔记(二) 信号量、条件变量、互斥量、读写锁

在有了进程和线程的模型之后,一个很大的问题就摆在眼前:进程和线程的执行顺序是不可预知的,那么,如何使得两个进程按照我们想要的顺序执行,从而得出正确的结果呢?

竞争条件:两个或者多个进程读写某些共享数据,最后的结果依赖于进程运行的精确时序。

临界区:把对共享内存进行访问的程序片段称作临界区。如果能使两个进程不可能同时处于临界区内,就能够避免竞争。

先引入一个经典的进程同步问题:生产者-消费者问题

生产者-消费者问题:有一个缓冲区,一个(或多个)进程在生产某种产品,它生产的东西会放入缓冲区内;一个(或多个)进程在消费产品,它会从缓冲区内取走产品。当缓冲区满时,生产者应当暂时停止生产;当缓冲区为空时,消费者应当暂时停止消费。

很显然,这个问题用简单的判断缓冲区是否为0或N是无法解决的。如果在消费者判断缓冲区为0时,恰好遇到了进程切换,生产者进程开始运行,此时应当唤醒消费者,然而这个信号丢失了,因为切换到消费者才进行了睡眠。这时,生产者会不断运行,直到缓冲区满,两个进程全部睡眠,造成了死锁。代码如下:

#define N 1000
int count=0;
void producer(void)
{
    int item;
    while(TRUE)
    {
        item=produce_item();
        if(count==N) sleep();//一段时间后,缓冲区满,生产者进程也睡眠了
        insert_item(item);
        count=count+1;
        if(count==1) wakeup(consumer);//设想判断条件成立时,切换了进程,再次切回时,唤醒消费者进程,然而消费者进程此时没有睡眠,信号丢失
    }        
}

void consumer(void)
{
    int item;
    while(TRUE)
    {
        if(count==0) sleep();//第一次count=1,消费者进程不会睡眠;第二次确实睡眠了
        item=remove_item();
        count=count-1;//此时缓冲区确实为空了
        if(count==N-1) wakeup(producer);
        consume_item(item);
    }
}

 

一、信号量

信号量是一种数据结构,可以理解为一个用来计数的整数和一个队列。整数用来记录唤醒次数,而队列被用来记录因为该信号量而阻塞的进程。

信号量只支持两种操作:P/V操作

P操作,可以理解为测试并减一。P(signal1),如果signal1大于0,那么把它减一,进程继续执行;如果signal为0,那么执行P操作的进程将会被阻塞,从而变为阻塞态,添加到因为signal1信号而阻塞的进程队列中。

V操作,可以理解为+1并唤醒。V(signal1)后,如果signal1本来就大于0,那么执行+1;如果有进程在该信号量上被阻塞,那么从队列中根据某种策略选择一个进程唤醒。如果多个进程在该信号量上阻塞,那么V操作后,signal1仍然可能为负数。

需要注意的是,P/V操作均应当是原子操作,即作为一个整体执行而不会被打断。

有了信号量,我们再来看生产者-消费者问题:

#define N 1000
typedef int semaphore;
semaphore mutex=1;//控制对临界区的访问,其实就是互斥量
semaphore empty=N;//表示空槽的数量
semaphore full=0;//填满的槽的数量
int count=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);
    }
}

有了信号量,这个问题就好解决多了:用信号量full、empty来表示已用和未用的数量,这样不管是满了还是空了,都不会造成死锁的问题。mutex的操作就是我们接下来要介绍的互斥锁。

 

二、互斥锁

互斥量其实可以理解为一个简化的信号量,它只有两种状态:0和1。互斥锁是用来解决进程(线程)互斥问题的。所谓进程互斥,就是两个进程实际上是一种互斥的关系,两者不能同时访问共享资源。

互斥量和信号量原理比较类似,一旦一个线程获得了锁,那么其它线程就无法访问共享资源,从而被阻塞,直到该线程交还出了锁的所有权,另外一个线程才能获得锁。

互斥锁的例子就不再给出,上面程序中已经有了,下面的程序中也会出现。

 

三、条件变量

条件变量是另外一种同步机制,可以用于线程和管程中的进程互斥。通常与互斥量一起使用。

条件变量允许线程由于一些暂时没有达到的条件而阻塞。通常,等待另一个线程完成该线程所需要的条件。条件达到时,另外一个线程发送一个信号,唤醒该线程。

条件变量对应的一组操作是pthread_cond_wait和pthread_cond_signal。

条件变量与互斥量一起使用,一般情况是:一个线程锁住一个互斥量,然后当它不能获得它期待的结果时,等待一个条件变量;最后另外一个线程向它发送信号,使得它可以继续执行。

需要注意的是,pthread_cond_wait会暂时解开持有的互斥锁。

 

四、读写锁

读写锁相对上面的问题会复杂一些,它被用来解决一个经典的问题:读者-写者问题

读写锁与互斥量类似,不过读写锁允许更高的并行性。互斥量要么是锁住状态要么是不加锁状态,而且一次只有一个线程可以对其加锁。

下面的代码考虑的是读者优先的读者-写者问题,对于共享区域的读写规则如下:

1.只要有一个读者在读,后来的读者可以进入共享区直接读。

2.只要有一个读者在读,写者就必须阻塞,直到最后一个读者离开。

3.不考虑抢占式,写者在写时,即使有读者到达,也会在就绪态等待。

 

typedef int semaphore;
semaphore mutex=1;    //互斥锁,控制对rc的访问
semaphore db=1;        //控制对数据库的访问
int rc=0;        //当前读者计数

void reader(void)
{
    while(TRUE)
    {
        down(&mutex);//加锁
        rc=rc+1;
        if(rc==1) down(&db);//第一个读者,加锁
        up(&mutex);
        read_data_base();
        down(&mutex);
        rc=rc-1;
        if(rc==0) up(&db);//最后一个读者离开,解锁
        up(&mutex);
        use_data_read();
    }
}

void writer(void)
{
    while(TRUE)
    {
        think_up_data();
        down(&db);//获取数据库访问的锁
        write_data_base();
        up(&db);
    }
}

 

这里,我们其实用了两个互斥锁来实现了读写锁。一个互斥锁用来保护共享区,另外一个互斥锁用来保护读者计数器。

 

读写锁可以由三种状态:读模式下加锁状态写模式下加锁状态不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。

在读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是如果线程希望以写模式对此锁进行加锁,它必须阻塞直到所有的线程释放读锁。虽然读写锁的实现各不相同,但当读写锁处于读模式锁住状态时,如果有另外的线程试图以写模式加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。

读写锁非常适合于对数据结构读的次数远大于写的情况。当读写锁在写模式下时,它所保护的数据结构就可以被安全地修改,因为当前只有一个线程可以在写模式下拥有这个锁。当读写锁在读状态下时,只要线程获取了读模式下的读写锁,该锁所保护的数据结构可以被多个获得读模式锁的线程读取。

读写锁也叫做共享-独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当他以写模式锁住时,它是以独占模式锁住的。

五、总结

这里,主要是简单总结一下这几种同步量的用法。

1、互斥锁只用在同一个线程中,用来给一个需要对临界区进行读写的操作加锁。

2、信号量与互斥量不同的地方在于,信号量一般用在多个进程或者线程中,分别执行P/V操作。

3、条件变量一般和互斥锁同时使用,或者用在管程中。

4、互斥锁,条件变量都只用于同一个进程的各线程间,而信号量(有名信号量)可用于不同进程间的同步。当信号量用于进程间同步时,要求信号量建立在共享内存区。

5、互斥锁是为上锁而优化的;条件变量是为等待而优化的; 信号量既可用于上锁,也可用于等待,因此会有更多的开销和更高的复杂性。

 

 

参考书籍:《现代操作系统》

 

posted @ 2017-10-18 21:21  luStar  阅读(6658)  评论(0编辑  收藏  举报