1.生产者与消费者的概念
生产者与消费者(Producer-Consumer)问题,是一个经典的并发编程问题。在这个问题中,涉及到两类进程:生产者和消费者。生产者负责生产数据,并将其放入一个缓冲区中;消费者则从缓冲区中取出数据并消费。
生产者和消费者通常是并发运行的,即它们可能在不同的时间点执行。生产者可能会在生产数据后立即放入缓冲区,而消费者可能会在稍后的时间点从缓冲区中取出数据。因此,需要一种机制来同步生产者和消费者的行为,以确保缓冲区不会溢出(即生产者不会在缓冲区满时继续生产数据)或下溢(即消费者不会在缓冲区空时尝试取出数据)。
生产者与消费者模型是一个经典的并发编程模型,用于解决多线程或进程间的数据共享和同步问题。该模型涉及两类对象:生产者和消费者,以及一个共享的数据缓冲区。
生产者:负责生产数据,并将这些数据放入共享缓冲区中。生产者可以在任何时间点生产数据,并将其添加到缓冲区,供消费者使用。
消费者:负责从共享缓冲区中取出数据,并进行消费(或处理)。消费者可以在任何时间点从缓冲区中取出数据进行处理。
共享缓冲区:是一个中介,用于存储生产者生产的数据,供消费者使用。这个缓冲区通常是有限的,因此生产者和消费者需要一种机制来同步他们的行为,以防止缓冲区溢出(生产者生产的数据太多,消费者来不及消费)或下溢(消费者消费的数据太多,生产者来不及生产)。
生产者-消费者模型中的关键概念和原则包括:
- 互斥:生产者和消费者需要互斥地访问共享缓冲区,以防止数据竞争和不一致的状态。这通常通过使用互斥锁(mutex)或其他同步机制来实现。
- 同步:生产者和消费者之间的同步是确保数据正确流动的关键。生产者不能在缓冲区满时继续生产数据,而消费者也不能在缓冲区空时尝试取出数据。这可以通过使用信号量(semaphores)、条件变量(condition variables)或其他同步机制来实现。
- 死锁和饥饿:在生产者-消费者模型中,需要特别注意避免死锁和饥饿问题。死锁是指多个线程或进程互相等待对方释放资源,导致无法继续执行。饥饿是指某些线程或进程长时间得不到所需的资源,而其他线程或进程却能得到足够的资源。为了避免这些问题,需要合理设计同步机制,并确保资源的公平分配。
初步实现生产者和消费者模型:
使用链表的数据结构,生产者生产数据,即不断地创建新节点,然后添加到链表中。
首先创建链表的数据结构,然后初始化一个头节点。
为了生产者和消费者需要互斥地访问共享缓冲区,那么创建一个互斥量 mutex
// 创建一个互斥量 pthread_mutex_t mutex; //链表节点数据结构 struct Node{ int num; struct Node *next; }; // 头结点 struct Node * head = NULL;
生产者采用头插法向链表中添加数据
//生产者生产数据 void * producer(void * arg) { // 不断的创建新的节点,添加到链表中 while(1) { pthread_mutex_lock(&mutex); struct Node * newNode = (struct Node *)malloc(sizeof(struct Node)); newNode->next = head; head = newNode; newNode->num = rand() % 1000; //随机值 printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self()); pthread_mutex_unlock(&mutex); usleep(100); } return NULL; }
消费者需要读取缓冲区中的数据,然后删除节点,在删除的时候,为了避免溢出,需要先判断链表中是否还有节点可以删除,即if(head != NULL)
//消费者,取内容 void * customer(void * arg) { while(1) { pthread_mutex_lock(&mutex); // 保存头结点的指针 struct Node * tmp = head; // 判断是否有数据 if(head != NULL) { // 有数据 head = head->next; printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self()); free(tmp); pthread_mutex_unlock(&mutex); usleep(100); } else { // 没有数据 pthread_mutex_unlock(&mutex); } } return NULL; }
整体代码中,创建5个生产者线程,和5个消费者线程。
/*
生产者消费者模型(粗略的版本)
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
// 创建一个互斥量
pthread_mutex_t mutex;
struct Node{
int num;
struct Node *next;
};
// 头结点
struct Node * head = NULL;
//生产者读数据
void * producer(void * arg) {
// 不断的创建新的节点,添加到链表中
while(1) {
pthread_mutex_lock(&mutex);
struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
newNode->next = head;
head = newNode;
newNode->num = rand() % 1000; //随机值
printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
pthread_mutex_unlock(&mutex);
usleep(100);
}
return NULL;
}
//消费者,取内容
void * customer(void * arg) {
while(1) {
pthread_mutex_lock(&mutex);
// 保存头结点的指针
struct Node * tmp = head;
// 判断是否有数据
if(head != NULL) {
// 有数据
head = head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
pthread_mutex_unlock(&mutex);
usleep(100);
} else {
// 没有数据
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);
// 创建5个生产者线程,和5个消费者线程
pthread_t ptids[5], ctids[5];
for(int i = 0; i < 5; i++) {
pthread_create(&ptids[i], NULL, producer, NULL);
pthread_create(&ctids[i], NULL, customer, NULL);
}
for(int i = 0; i < 5; i++) {
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
while(1) {
sleep(10);
} //防止子线程还没有执行完,互斥量就释放了
pthread_mutex_destroy(&mutex);
pthread_exit(NULL); //主线程退出
return 0;
}
上述代码的执行没有什么问题。但是,对于消费者来说,如果一直没有数据,那么它抢占到资源之后,会一直加锁,判断,解锁。这样就会对资源造成浪费。如果没有数据,那就通知生产者生产。为了解决这个问题,下文引入两种机制。
2.条件变量
条件变量(Condition Variables)是线程同步的一种机制,它允许一个或多个线程等待某个条件成立,或者在某个条件成立后唤醒一个或多个等待的线程。条件变量通常与互斥锁(Mutex)一起使用,以避免竞争条件和保证线程安全。
条件变量的主要操作包括:
- 等待(Wait):线程调用条件变量的等待函数,释放已持有的互斥锁,并进入等待状态。此时,线程不会消耗CPU资源,直到被唤醒或超时。在等待期间,如果条件变量接收到唤醒信号,线程会重新获取互斥锁,并检查条件是否满足。
- 唤醒(Signal)或通知(Notify):当条件满足时,另一个线程会调用条件变量的唤醒函数,以唤醒一个或多个等待的线程。被唤醒的线程会重新获取互斥锁,并再次检查条件是否满足。
条件变量的使用需要注意以下几点:
- 条件变量和互斥锁必须一起使用,互斥锁用于保护共享数据,条件变量用于同步线程。条件变量自己不能保证安全问题。
- 在调用等待函数之前,线程必须已经获取了互斥锁,因为调用等待函数需要释放锁。在等待函数返回后,线程会重新获取互斥锁。
- 条件变量只能唤醒等待的线程,不能强制线程退出等待状态。如果条件不满足,线程会再次进入等待状态。
- 条件变量支持“唤醒一个”或“唤醒所有”等待线程的操作。唤醒线程后,它会重新获取当线程进入休眠状态时释放的锁。
条件变量的类型 pthread_cond_t
◼ int pthread_cond_init(pthread_cond_t *restrict cond, const
pthread_condattr_t *restrict attr);
◼ int pthread_cond_destroy(pthread_cond_t *cond);
◼ int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
◼ 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);
使用条件变量修改上述代码:
/*
条件变量的类型 pthread_cond_t
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
- 等待,调用了该函数,线程会阻塞。
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);
- 唤醒所有的等待的线程
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
// 创建一个互斥量
pthread_mutex_t mutex;
// 创建条件变量
pthread_cond_t cond;
struct Node{
int num;
struct Node *next;
};
// 头结点
struct Node * head = NULL;
void * producer(void * arg) {
// 不断的创建新的节点,添加到链表中
while(1) {
pthread_mutex_lock(&mutex);
struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
newNode->next = head;
head = newNode;
newNode->num = rand() % 1000;
printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
// 只要生产了一个,就通知消费者消费
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
usleep(100);
}
return NULL;
}
void * customer(void * arg) {
while(1) {
pthread_mutex_lock(&mutex);
// 保存头结点的指针
struct Node * tmp = head;
// 判断是否有数据,否则会出现段错误
if(head != NULL) {
// 有数据,则消费
head = head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
pthread_mutex_unlock(&mutex);
usleep(100);
} else {
// 没有数据,需要等待,当生产者生产之后,就不阻塞了
// 当这个函数调用阻塞等待的时候,会对互斥锁进行解锁,否则生产者拿不到互斥锁。
//解除阻塞时,重新加锁
//当不阻塞的,继续向下执行,会重新加锁。
pthread_cond_wait(&cond, &mutex);
pthread_mutex_unlock(&mutex);
}
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
// 创建5个生产者线程,和5个消费者线程
pthread_t ptids[5], ctids[5];
for(int i = 0; i < 5; i++) {
pthread_create(&ptids[i], NULL, producer, NULL);
pthread_create(&ctids[i], NULL, customer, NULL);
}
for(int i = 0; i < 5; i++) {
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
while(1) {
sleep(10);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
pthread_exit(NULL);
return 0;
}
3.信号量
信号量(Semaphore)是一种用于控制多个线程或进程对共享资源访问的同步机制。它可以看作是一个计数器,用于表示可用资源的数量。信号量的主要操作包括P操作(等待)和V操作(释放)。
P操作(Wait):当一个线程或进程需要访问共享资源时,它首先会执行P操作。这个操作会将信号量的值减1,表示一个资源被占用。如果信号量的值大于0,表示还有可用资源,线程或进程可以继续执行;如果信号量的值为0,表示没有可用资源,线程或进程将被阻塞,直到有资源可用。
V操作(Signal):当一个线程或进程完成对共享资源的访问后,它会执行V操作。这个操作会将信号量的值加1,表示一个资源被释放。如果有其他线程或进程正在等待该资源(即被P操作阻塞),那么它们将被唤醒并继续执行。
信号量的主要特点包括:
- 计数器功能:信号量本质上是一个计数器,用于表示可用资源的数量。
- 互斥性:通过控制对共享资源的访问,信号量可以实现互斥性,确保同一时间只有一个线程或进程访问共享资源。
- 同步性:信号量可以用于同步多个线程或进程的操作,确保它们按照预定的顺序访问共享资源。
信号量的使用需要注意以下几点:
- 在多线程或多进程环境中,需要确保对信号量的操作是原子的,即在一个操作完成之前不会被其他线程或进程打断。这通常通过使用互斥锁或其他同步机制来实现。
- 在创建信号量时,需要指定初始值,表示可用资源的数量。这个值应该根据实际情况进行设置,以确保不会发生资源耗尽的情况。
- 需要避免死锁和饥饿问题。例如,可以使用“超时”机制来避免线程或进程无限期地等待资源。
信号量的类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 初始化信号量
- 参数:
- sem : 信号量变量的地址
- pshared : 0 用在线程间 ,非0 用在进程间
- value : 信号量中的值,生产+1,消费-1
int sem_destroy(sem_t *sem);
- 释放资源
int sem_wait(sem_t *sem);
- 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞,直到大于0(post)
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
- 对信号量解锁,调用一次对信号量的值+1
int sem_getvalue(sem_t *sem, int *sval);
//假设资源数为8
sem_t psem;
sem_t csem;
init(psem, 0, 8); //生产者所拥有的资源数最开始为8
init(csem, 0, 0); //最开始的时候,没有可以供消费者消费的数据,因此为0
producer() {
sem_wait(&psem); 8-1=7
sem_post(&csem) //通知消费者消费 1
}
customer() {
sem_wait(&csem); 0等 有则消费1-1=0
sem_post(&psem) //通知生产者 7+1
}
总体代码:
/*
信号量的类型 sem_t
int sem_init(sem_t *sem, int pshared, unsigned int value);
- 初始化信号量
- 参数:
- sem : 信号量变量的地址
- pshared : 0 用在线程间 ,非0 用在进程间
- value : 信号量中的值,生产+1,消费-1
int sem_destroy(sem_t *sem);
- 释放资源
int sem_wait(sem_t *sem);
- 对信号量加锁,调用一次对信号量的值-1,如果值为0,就阻塞,直到大于0(post)
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
- 对信号量解锁,调用一次对信号量的值+1
int sem_getvalue(sem_t *sem, int *sval);
sem_t psem;
sem_t csem;
init(psem, 0, 8);
init(csem, 0, 0);
producer() {
sem_wait(&psem); 8-1=7
sem_post(&csem) //通知消费者消费 1
}
customer() {
sem_wait(&csem); 0等 1-1=0
sem_post(&psem) //通知生产者 7+1
}
*/
#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
// 创建一个互斥量
pthread_mutex_t mutex;
// 创建两个信号量
sem_t psem;
sem_t csem;
struct Node{
int num;
struct Node *next;
};
// 头结点
struct Node * head = NULL;
void * producer(void * arg) {
// 不断的创建新的节点,添加到链表中
while(1) {
sem_wait(&psem);
pthread_mutex_lock(&mutex);
struct Node * newNode = (struct Node *)malloc(sizeof(struct Node));
newNode->next = head;
head = newNode;
newNode->num = rand() % 1000;
printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
pthread_mutex_unlock(&mutex);
sem_post(&csem);
}
return NULL;
}
void * customer(void * arg) {
while(1) {
sem_wait(&csem); //0时等待,有的时候在消费,不用等待了
pthread_mutex_lock(&mutex);
// 保存头结点的指针
struct Node * tmp = head;
head = head->next;
printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
free(tmp);
pthread_mutex_unlock(&mutex);
sem_post(&psem);
}
return NULL;
}
int main() {
pthread_mutex_init(&mutex, NULL);
sem_init(&psem, 0, 8);
sem_init(&csem, 0, 0);
// 创建5个生产者线程,和5个消费者线程
pthread_t ptids[5], ctids[5];
for(int i = 0; i < 5; i++) {
pthread_create(&ptids[i], NULL, producer, NULL);
pthread_create(&ctids[i], NULL, customer, NULL);
}
for(int i = 0; i < 5; i++) {
pthread_detach(ptids[i]);
pthread_detach(ctids[i]);
}
while(1) {
sleep(10);
}
pthread_mutex_destroy(&mutex);
pthread_exit(NULL);
return 0;
}
问题:
为何信号量和互斥锁调换位置就会出现死锁呢?
假设一直都是生产者线程抢占到cpu已经生产了8个(我们设置的psem为8),然后接下来又是生产者抢占到cpu,然后对互斥量上锁,在sem_wait函数阻塞,阻塞后cpu空闲,cpu去执行消费者线程,而消费者需要拿锁,但是锁在上一个生产者的手中,导致消费者卡在拿锁的函数,形成死锁。
生产者的psem=0,在wait阻塞,消费者拿不到锁产生死锁
消费者的csem=0,在wait阻塞,生产者拿不到锁产生死锁