问题描述

一组生产者线程程和一组消费者线程程共享一个初始为空、大小为n的缓冲区,只有缓冲区没满时,生产者才能把消息放入到缓冲区,否则必须等待;只有缓冲区不空时,消费者才能从中取出消息,否则必须等待。由于缓冲区是临界资源,它只允许一个生产者放入消息,或者一个消费者从中取出消息。


问题分析

生产者和消费者对缓冲区互斥访问是互斥关系,同时生产者和消费者又是一个相互协作的关系,只有生产者生产之后,消费者才能消费,他们也是同步关系。这里的核心在于***不让生产者在缓存满时仍写入数据,不让消费者在缓存空时仍读取数据 ***。


实现方案

信号量方式

信号量概念:

信号量是IPC结构中的一种,是进程间通信的一种方法,也可以解决同一进程不同线程之间的通信问题。它是用来保证两个或多个关键代码段不被并发调用,防止多个进程同时对共享资源进行操作。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

关键函数:

sem_post
函数原型:int sem_post(sem_t *sem)
作用是给信号量的值加上一个“1”。 当有线程阻塞在这个信号量上时,调用这个函数会使其中一个线程不在阻塞,选择机制是有线程的调度策略决定的。
sem_wait
函数原型:int sem_wait(sem_t * sem)
它的作用是从信号量的值减去一个“1”,但它永远会先等待该信号量为一个非零值才开始做减法。

下面来看一下该方案的代码实现:

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

#define MAX 5

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
sem_t full;
sem_t empty;

int top = 0;
int bottom = 0;

void *produce(void *args)
{
    int i;
    for(i=0; i<MAX*2; i++)
    {
        printf("producer is preparing data\n");

        //sem_wait得到资源后将会对信号量减一,得不到就会阻塞
        //若空闲缓冲区满,阻塞;若不满,则会对empty减一
        sem_wait(&empty);

        //互斥锁防止多个线程同时对队列操作
        pthread_mutex_lock(&mutex);

        top = (top + 1) % MAX;
        printf("now top is: %d\n", top);

        //解锁
        pthread_mutex_unlock(&mutex);

        //满缓冲区加一
        sem_post(&full);
    }
    return (void*)1;
}

void *consume(void *args)
{
    int i;
    for (i=0; i<MAX*2; i++)
    {
        //若full为空,则一直阻塞,不让消费者读取数据
        sem_wait(&full);

        pthread_mutex_lock(&mutex);

        bottom = (bottom+1) % MAX;
        printf("now bottom is: %d\n", bottom);

        pthread_mutex_unlock(&mutex);

        //空缓冲区加一
        sem_post(&empty);
    }
    return (void*)2;
}

int main(int argc, char *argv[])
{
    pthread_t thid1;
    pthread_t thid2;

    int  ret1;
    int  ret2;

    sem_init(&full, 0, 0);
    sem_init(&empty, 0, MAX);

    pthread_create(&thid1, NULL, produce, NULL);
    pthread_create(&thid2, NULL, consume, NULL);

    pthread_join(thid1, (void**)&ret1);
    pthread_join(thid2, (void**)&ret2);

    return 0;
}

思考一下如果把sem_wait()和sem_post()放到pthread_mutex_lock()与pthread_mutex_unlock()之间会如何呢?
我们知道我们无法控制线程进入共享区的顺序,如果消费者线程先对mutex加锁,进入共享区后,sem_wait发现full队列为空,则会一直阻塞在那里;这时生产者线程试图对mutex加锁,发现mutex已经被锁住,也会阻塞等待mutex解锁,这样双方就永远无法唤醒对方,从而造成死锁

条件变量方式

条件变量概念:

条件变量(cond)是在多线程程序中用来实现”等待–>唤醒”逻辑常用的方法。条件变量利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待”条件变量的条件成立”而挂起;另一个线程使“条件成立”。

关键函数:

thread_cond_wait:
pthread_cond_wait() 所做的第一件事就是同时对互斥对象解锁(于是其它线程可以修改已链接列表),并等待条件 mycond 发生(这样当 pthread_cond_wait() 接收到另一个线程的“信号”时,它将苏醒)。现在互斥对象已被解锁,其它线程可以访问和修改已链接列表,可能还会添加项。
pthread_cond_signal:
在某些情况下,活动线程只需要唤醒第一个正在睡眠的线程。假设您只对队列添加了一个工作作业。那么只需要唤醒一个工作程序线程,该函数正实现了此功能,只唤醒一个线程。

下面来看一下该方案的实现代码:

pthread_cond_t notfull = PTHREAD_COND_INITIALIZER;
pthread_cond_t notempty = PTHREAD_COND_INITIALIZER;

void *produce(void *args)
{
    int i;
    for(i=0; i<MAX*2; i++)
    {
        pthread_mutex_lock(&mutex);

        while((top+1)%MAX == bottom)
        {
            printf("full! producer is waiting\n");
            //先将互斥锁解开,陷入阻塞,直到pthread_cond_signal发出信号后再加锁
            //等待消费者发出队列不满的信号
            pthread_cond_wait(notfull, &mutex);
        }

        top = (top+1) % MAX;
     //发出队列非空的消息
        pthread_cond_signal(notempty);

        pthread_mutex_unlock(&mutex);
    }
    return (void*)1;
}

void *consume(void *args)
{
    int i;
    for(i=0; i<MAX*2; i++)
    {
        pthread_mutex_lock(&mutex);

        while(top%MAX == bottom)
        {
            printf("full! producer is waiting\n");
            //等待生产者发出队列不空的信号
            pthread_cond_wait(notempty, &mutex);
        }

        top = (top+1) % MAX;

        //发出队列不满的消息
        pthread_cond_signal(notfull);

        pthread_mutex_unlock(&mutex);
    }
    return (void*)1;
}

发表评论

电子邮件地址不会被公开。 必填项已用*标注