线程间竞争
一、基本概念
原子操作:中途不会被打断的操作称为原子操作(不会被其他线程竞争影响的操作)
竞争与同步:
同一个进程中的线程共享进程中绝大多数资源,当他们随意竞争时,可能导致资源被破坏、脏数据、不完整、不一致的情况
通过一些方法让线程在竞争资源时相互协调,避免出现以上情况,这种线程间协同工作成为线程同步
临界区和临界资源:
被多个线程同时访问的代码称为临界区、被同时访问的资源称为临界资源
二、互斥量(互斥锁)
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中的信号量原理相同,相当于线程之间使用的同一个计数器,用于统计和控制访问有限资源的线程数量
#include <semaphore.h>
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时间,如果超时则返回ETIMEOUT错误编码
int sem_post(sem_t *sem);
功能:对信号量+1
int sem_destroy(sem_t *sem);
功能:销毁信号量
四、死锁
1、什么是死锁
多个进程或者线程之间互相等待对方手中的资源,在得到新的资源之前不会主动释放自己手中的资源,这样如果形成了等待环路,称之为死锁现象
2、产生死锁的四大必要条件
资源互斥:资源只有两种状态,只有可用和不可用状态,不能同时使用,同一时间内只能被一个进程或线程使用
占有且请求:对已经得到的资源的进程或线程,对资源保持占有,并继续请求新的资源
资源不可剥夺:资源已经分配给进程或线程后,不能被其他进程或线程强制性获取、除非资源的占有者主动释放
环路等待:当死锁发生时,系统中必定有两个或两个以上的进程或线程执行路线形成一条等待环路
注意:以上四个条件同时成立,就会形成死锁,死锁一旦产生基本无解,以现在的操作系统是无法解决死锁,因此只能防止死锁的产生
3、如何防止死锁的产生
破坏资源互斥:
想办法让资源能够共享使用
缺点:受显示环境和资金的影响无法让资源共享
破坏占有且请求:
采用预分配的方式,让进程或线程在运行前一次性申请所有资源,如果在资源没有满足时不投入运行
缺点:系统资源的占用会严重浪费,因为有些资源可能开始时使用,但是有些资源可能最后才使用
破坏资源不可剥夺:
当一个进程或线程已经占有一个不可被剥夺的资源,并且在请求新资源无法被满足时,则释放已经占用的资源,等待一段时间后重新申请
缺点:该策略实现比较麻烦,而且释放已经申请的资源可能会导致前一阶段的工作无效,反复的申请释放资源也会增加系统开销、占用CPU和寄存器、内存等资源
破坏等待环路:
给每个资源起编号,进程或线程按照编号顺序依次请求资源,并且只有拿到前一个资源,才能继续请求下一个资源
缺点:资源的编号相对稳定,当资源增加或删除时受到很大影响
4、如何判断死锁
1、画出资源分配图
2、简化资源分配图
3、使用死锁判断原理:如果没有环路一定不会出现死锁
了解:银行家算法
五、条件变量
当某些条件满足时,可让线程自己进入睡眠,也可以当某些条件满足时,唤醒正在睡眠的线程
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)