生产者消费者模型底层逻辑与实现
一、基本概念
原子操作:中途不会被打断的操作称为原子操作(不会被其他线程竞争影响的操作)
竞争与同步:
竞争:同一个进程中的线程共享进程中绝大多数资源,当它们随意竞争时可能导致资源被破坏、脏数据、不完整、不一致的情况
同步:通过一些方法让线程在竞争资源时相互协调,避免出现以上情况,这种线程间协同工作成为线程同步
二、互斥量(互斥锁)
注意:
有些系统的man手册没有关于mutex的文档需要安装:
sudo apt-get install manpages-posix-dev
pthread_mutex_t 是一种数据类型 可以定义互斥量变量
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
功能:初始化一个互斥量
mutex:要初始化的互斥量变量
attr:对互斥量的属性进行设置,一般给NULL即可
注意:一般默认是开锁状态,也可使用PTHREAD_MUTEX_INITIALIZER对互斥量变量进行初始化
例如:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:对互斥量进行加锁,成功则继续执行下文,失败则阻塞,直到互斥量被解锁并加锁成功,才返回
int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:对互斥量尝试加锁,成功(0)或者失败(EBUSY)都立即返回
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:对互斥量解锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁互斥量
三、信号量
与XSI中的信号量原理相同,相当于线程之前使用的同一个计数器,用于统计、控制访问有限的共享资源的线程数量
int sem_init(sem_t *sem,int pshared,unsigned int value);
功能:初始化信号量
sem:被初始化的信号量
pshared:
0 只能在本进程内使用
非0 表示该信号量可以以共享内存的形式,让多个进程共享使用 (Linux不支持)
value:信号量的初始值
int sem_wait(sem_t *sem);
功能:对信号量-1,如果信号量为0不够减,则阻塞,减成功则继续执行
int sem_trywait(sem_t *sem);
功能:对信号量尝试-1,成功(0)或失败(EAGAIN)都立即返回
int sem_timedwait(sem_t *sem,const struct timespec *abs_timeout);
功能:对信号量减1,如果不够减则等待abs_timeout时间,如果超时返回ETIMEDOUT错误编码
int sem_post(sem_t *sem);
功能:对信号量加1
int sem_destroy(sem_t *sem);
功能:销毁信号量
四、死锁
死锁: 多个进程或者线程之间互相等待对方手中的资源,在得到新的资源之间不会主动释放自己手中的资源,这样如果形成了等待环路,称之为死锁现象。
产生死锁的四大必要条件:
- 资源互斥:资源只有两种状态,只有可用和不可用状态,不能同时使用,同一时间内只能被一个进程或线程使用
- 占有且请求:对已经得到资源的进程或线程,对旧资源保持占有并继续请求新的资源
- 资源不可剥夺:资源已经分配给进程或线程后,不能被其它进程或线程强制性获取、除非资源的占有者主动释放
- 环路等待:当死锁发生时,系统中必行有两个或两个以上的进程或线程执行路线形成一条等待环路
注意:以上四个条件同时成立,就会形成死锁,死锁一旦产生基本无解,以现在的操作系统是无法解决死锁,因此只能防止死锁的产生。(试探评估法通过杀死一些进程监控别的进程运行状态是一种陷入死锁后的解决方案之一)
如何防止死锁的产生:
破坏资源互斥:
1.想办法让资源能够共享使用
缺点:受现实环境和资金的影响无法让资源共享
2.破坏占用且请求:
采用预分配的方式,让进程或线程在运行前一次性申请所有资源,如果在资源没有满足时不投入运行
缺点:系统资源的占用会严重浪费,因为有些资源可能开始时使用,但是有些资源可能最后才使用
3.破坏资源不可剥夺:
当一个进程或线程已经占用一个不可被剥夺的资源,并且在请求新资源无法被满足时,则释放已经占用的资源,等待一段时间后重新申请
缺点:该策略实现比较麻烦,而且释放已经申请的资源可能会导致前一阶段的工作无效,反复地申请释放资源也会增加系统开销、占用CPU和寄存器、内存等资源
4.破坏等待环路:
给每个资源起编号,进程或线程按照编号顺序依次请求资源,并且只有拿到前一个资源,才能继续请求下一个资源
缺点:资源的编号相对稳定,当资源增加或删除时收到很大影响
5.了解银行家算法。
如何判断死锁:
- 画出资源分配图
- 简化资源分配图
- 使用死锁判断原理:如果没有环路一定不会出现死锁
五、条件变量
概念:当某些条件满足时,可让线程自己进入睡眠,也可以当某些条件满足时唤醒正在睡眠的线程
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
功能:初始化条件变量
cond:要初始化的条件变量
attr:默认给NULL即可
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
注意:也可以使用PTHREAD_COND_INITIALIZER赋值方式初始化
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
功能:让当前线程睡入cond,并解锁mutex
返回值:直到线程被唤醒才返回
int pthread_cond_signal(pthread_cond_t *cond);
功能:唤醒cond中正在睡眠的一个线程,在唤醒前要确保锁处于打开状态,当线程醒来时该线程会自动把锁重新加上
int pthread_cond_broadcast(pthread_cond_t *cond);
功能:唤醒cond中所有线程,线程是否醒来取决于能否再次加锁
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
功能:让当前线程睡入cond,并解锁mutex,只睡abstime时间,超时会被操作系统唤醒
int pthread_cond_destroy(pthread_cond_t *cond);
功能:销毁条件变量
注意:使用条件变量可以实现生产者与消费者模型
六、生产者与消费者模型(线程池、数据池)
生产者:生产数据的线程
消费者:使用数据的线程
仓库:临时存放数据的缓冲区(仓库解决了生产、消费不匹配)
可能产生的问题:
消费快于生产:仓库空虚、饿死
生产快于消费:仓库爆满、撑死
使用条件变量来解决以上问题:
当缓冲区空的时候,消费者线程睡入条件变量(empty),通知生产者线程全部醒来(full)
当缓冲区满的时候,生产者线程睡入条件变量(full),通知消费者线程全部醒来(empty)
详细代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
// 仓库容量
#define BUF_MAX 30
// 仓库
char buf[BUF_MAX];
// 仓库中数据的数量
int cnt = 0;
// 访问仓库的互斥量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 仓库满的条件变量
pthread_cond_t full = PTHREAD_COND_INITIALIZER;
// 仓库空的条件变量
pthread_cond_t empty = PTHREAD_COND_INITIALIZER;
// 显示仓库和上一步操作
void show_buf(const char* who,const char* ori,char data)
{
for(int i=0; i<cnt; i++)
{
printf("%c ",buf[i]);
}
printf("%s%s%c\n",who,ori,data);
}
// 消费者线程
void* consumer(void* arg)
{
const char* who = (const char*)arg;
for(;;)
{
pthread_mutex_lock(&mutex);
if(0 >= cnt)
{
printf("空仓!\n");
pthread_cond_wait(&empty,&mutex);
}
char data = buf[--cnt];
show_buf(who,"->",data);
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&full);
usleep(rand()%10*10000);
}
}
// 生产者线程
void* producer(void* arg)
{
const char* who = (const char*)arg;
for(;;)
{
// 加锁
pthread_mutex_lock(&mutex);
if(BUF_MAX <= cnt)
{
printf("满仓!\n");
// 睡入full并解锁
pthread_cond_wait(&full,&mutex);
}
// 生产数据
char data = 'A'+rand()%26;
// 新数据放入仓库中
buf[cnt++] = data;
// 显示仓库
show_buf(who,"<-",data);
// 解锁
pthread_mutex_unlock(&mutex);
// 此时仓库有数据,唤醒消费者进行消费
pthread_cond_signal(&empty);
usleep(rand()%10*10000);
}
}
int main(int argc,const char* argv[])
{
pthread_t tid[10];
// 创建5个生产者线程
for(int i=0; i<5; i++)
{
pthread_create(&tid[i],NULL,producer,"生产者");
}
// 创建5个消费者线程
for(int i=5; i<10; i++)
{
pthread_create(&tid[i],NULL,consumer,"消费者");
}
// 等待子线程结束
for(int i=0; i<10; i++)
{
pthread_join(tid[i],NULL);
}
}