首先我们要了解生产者消费者模型的运行机制,生产者线程用来收集用户数据,数据保存在共享内存区,消费者线程需要从共享内存中取出数据进行处理。
下面基于我们的代码,做一个简单的说明,为了简单化,生产者线程不直接去收集我们用户的数据,而是直接产生数据,我们让生产者从0开始一直生产自然数。消费者对数据的处理也仅仅是将自然数打印出来。共享内存我们使用链表,因为链表我们不用但是满的情况。下面是具体实现的代码(代码实现的是一个生产者和一个消费者)
代码:
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <pthread.h>
//生产者消费者模型,共享空间使用链表,不用担心链表满的情况。使用互斥锁
struct student
{
int nu;
struct student* next;
};
struct student *head;//定义链表的头节点
//定义一个互斥锁
pthread_mutex_t lock;
//定义一个条件变量
pthread_cond_t cond;
//生产者
void * producer(void * arg)
{
int i=0;
while(1)
{
struct student* node=(struct student*)malloc(sizeof(struct student*));
node->nu=i++;
printf("+++++生产者生产数据 i=%d\n",node->nu);
//要把数据写道链表中,采用头插法。需要加锁
pthread_mutex_lock(&lock);
node->next=head;
head=node;
pthread_mutex_unlock(&lock);//解锁
//生产者写了数据,head不为空,这是生产者肯定的事情,所以要在生产者里唤醒
//被条件变量阻塞的消费者
pthread_cond_signal(&cond);
usleep(50000);//避免打印太快看不出效果
}
return NULL;
}
//消费者
void* cosumer(void* arg)
{
struct student* node;
if(1)
{
pthread_mutex_lock(&lock);
while(head==NULL)
{
//head为空,说明生产者没有生产。阻塞。使用条件变量解决
pthread_cond_wait(&cond,&lock);
}
//摘取头节点
node=head;
head=head->next;
pthread_mutex_unlock(&lock);//解锁
printf("--------消费者处理数据 i=%d\n",node->nu);
free(node);//避免内存泄漏
usleep(50000);//避免打印太快看不出效果
}
return NULL;
}
//主线程什么都不做,只是定义
int main(int argc, char* argv[])
{
pthread_mutex_init(&lock,NULL);//初始化互斥锁
pthread_t p_tid;
pthread_t c_tid;
pthread_create(&p_tid,NULL,producer,NULL);
pthread_create(&c_tid,NULL,cosumer,NULL);
while(1);//线程是依托进程的,我们不让进程结束
pthread_mutex_destroy(&lock);
pthread_exit(NULL);
}
运行结果:
问题:在上面代码的基础上,如果我们增加多个消费者,而且生产者不变依然只有一个,会怎么样呢??
我们稍微改一改代码:把消费者变成5个,其他不变
//主线程什么都不做,只是定义
int main(int argc, char* argv[])
{
pthread_mutex_init(&lock,NULL);//初始化互斥锁
pthread_t p_tid;
pthread_t c_tid;
pthread_create(&p_tid,NULL,producer,NULL);
pthread_create(&c_tid,NULL,cosumer,NULL);
pthread_create(&c_tid,NULL,cosumer,NULL);
pthread_create(&c_tid,NULL,cosumer,NULL);
pthread_create(&c_tid,NULL,cosumer,NULL);
pthread_create(&c_tid,NULL,cosumer,NULL);
while(1);//线程是依托进程的,我们不让进程结束
pthread_mutex_destroy(&lock);
pthread_exit(NULL);
}
执行结果:
错误:很明显发生了段错误,段错误不一定发生,但是一直运行迟早会发生,这是为什么为什么呢????
分析原因:首先分析我们的代码为什么会出现段错误,可能会在那个地方出现。段错误(无效的地址访问),从我们代码可以看出当head=NULL时,执行了head=head->next。即对空节点访问了next。才可能出现段错误。
解释:首先假设执行到摸一个阶段当前链表head为空,五个消费者线程(不一定是五个,至少两个)都执行了,发现head为空,则阻塞在条件变量里面。这时候生产者写了一个数据,链表里面只有一个节点不为空。这时pthread_cond_signal唤醒消费者。问题就出在pthread_cond_signal函数上面。官方给的解释,包括man里面的解释也是,唤醒一个阻塞在条件变量上的线程,但是实际上它唤醒了一个或者多个。其实这是一个BUG。假设有两个消费者(A和B)都被pthread_cond_signal唤醒了。此时,A和B取消阻塞进行加锁,但是A/B中的一个能加锁成功,假设是A,则B阻塞在加锁上,A读走了唯一的数据,现在head=NULL。当A开锁的一瞬间,B加锁成功,执行读取操作,但是head=NULL。所以执行代码head=head->next是发生段错误。如果真的pthread_cond_signal只是唤醒了一个线程,就不会发生这种情况,事实证明pthread_cond_signal函数是唤醒一个或者多个线程,而不是只唤醒一个。
如何解决:把消费者中的 if(head==NULL)改成 while(head==NULL)。改成while后,线程B在A开锁的一瞬间加锁成功之后它会继续判断head==NULL的条件语句,而不是直接运行下面的代码。所以以后不管使用pthread_cond_signal还是pthread_cond_broadcast()最好都用while。