生产者消费者问题
使用条件变量解决
条件变量概念
条件变量视是线程可用的另一种同步机制。条件变量给多个线程提供了一个汇合的场所。条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生
条件本身是由互斥量保护的。线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁住以后才能计算条件。
相关函数
#include <pthread.h>
pthread_cond_t cond;
// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
//cond: 条件变量的地址 attr: 条件变量属性,一般使用默认属性,指定为 NULL
// 销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
//通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。
//该函数会对这个互斥锁做以下几件事情:
//1.在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁
//2.当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上,继续向下访问临界区
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
// 唤醒阻塞在条件变量上的线程, ××至少有一个××被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程××全部××解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);
组成
生产者线程 -> 若干个
生产商品或者任务放入到任务队列中
任务队列满了就阻塞,不满的时候就工作
通过一个生产者的条件变量控制生产者线程阻塞和非阻塞
消费者线程 -> 若干个
读任务队列,将任务或者数据取出
任务队列中有数据就消费,没有数据就阻塞
通过一个消费者的条件变量控制消费者线程阻塞和非阻塞
队列 -> 存储任务 / 数据,对应一块内存,为了读写访问可以通过一个数据结构维护这块内存
可以是数组、链表,也可以使用 stl 容器:queue /stack/list /vector
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
// 链表的节点
struct Node
{
int number;
struct Node* next;
};
// 定义条件变量, 控制消费者线程
pthread_cond_t cond;
// 互斥锁变量
pthread_mutex_t mutex;
// 指向头结点的指针
struct Node * head = NULL;
// 生产者的回调函数
void* producer(void* arg)
{
// 一直生产
while(1)
{
pthread_mutex_lock(&mutex);
// 创建一个链表的新节点
struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
// 节点初始化
pnew->number = rand() % 1000;
// 节点的连接, 添加到链表的头部, 新节点就新的头结点
pnew->next = head;
// head指针前移
head = pnew;
printf("+++producer, number = %d, tid = %ld\n", pnew->number, pthread_self());
pthread_mutex_unlock(&mutex);
// 生产了任务, 通知消费者消费
pthread_cond_broadcast(&cond);
// 生产慢一点
sleep(rand() % 3);
}
return NULL;
}
// 消费者的回调函数
void* consumer(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
// 一直消费, 删除链表中的一个节点
// if(head == NULL) // 这样写有bug
while(head == NULL)
{
// 任务队列, 也就是链表中已经没有节点可以消费了
// 消费者线程需要阻塞
// 线程加互斥锁成功, 但是线程阻塞在这行代码上, 锁还没解开
// 其他线程在访问这把锁的时候也会阻塞, 生产者也会阻塞 ==> 死锁
// 这函数会自动将线程拥有的锁解开
pthread_cond_wait(&cond, &mutex);
// 当消费者线程解除阻塞之后, 会自动将这把锁锁上
// 这时候当前这个线程又重新拥有了这把互斥锁
}
// 取出链表的头结点, 将其删除
struct Node* pnode = head;
printf("--consumer: number: %d, tid = %ld\n", pnode->number, pthread_self());
head = pnode->next;
free(pnode);
pthread_mutex_unlock(&mutex);
sleep(rand() % 3);
}
return NULL;
}
int main()
{
// 初始化条件变量
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
// 创建5个生产者, 5个消费者
pthread_t ptid[5];
pthread_t ctid[5];
for(int i=0; i<5; ++i)
{
pthread_create(&ptid[i], NULL, producer, NULL);
}
for(int i=0; i<5; ++i)
{
pthread_create(&ctid[i], NULL, consumer, NULL);
}
// 释放资源
for(int i=0; i<5; ++i)
{
// 阻塞等待子线程退出
pthread_join(ptid[i], NULL);
}
for(int i=0; i<5; ++i)
{
pthread_join(ctid[i], NULL);
}
// 销毁条件变量
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
参考
作者: 苏丙榅
链接: 爱编程的大丙