1 基本概念
① 竞争与同步
同一个进程中的线程能够共享进程中的绝大多数资源,当他们随意竞争时可能会导致共享资源被破坏、脏数据、不完整、不一致等问题
通过一些方法让进程中的线程在竞争资源时相互协调,避免出现以上问题,这种手段就称为线程同步技术
② 临界区:
能够被多个线程访问但又不能同时访问的代码片段称为临界区
③ 临界资源:
能够被多个进程访问但又不能同时防伪的资源叫做临界资源
当准备访问临界区和临界资源时,有别的线程和进程正在访问,那么会进入等待
④ 竞态条件:
当多个线程在临界区内执行且等待,由于线程执行的顺序有随机性,导致结果不确定,就称发生了竞态条件
⑤ 原子操作:
操作不能够被继续拆解、执行过程中不能被任何方式影响的操作,称为原子操作
2 互斥量(互斥锁)
注意:linux系统man手册中没有pthread_mutex_xxx系列函数说明,需要额外安装
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_t mutex = PTHREAD_MUTEX_INITIALIZER;
注意:可以直接使用PTHREAD_MUTEX_INITIALIZER给互斥锁初始化
int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:对一个互斥锁上锁
mutex:要上锁的互斥量
返回值:
成功则上锁并返回继续执行
失败则阻塞等待,直到互斥锁开锁并成功上锁才返回继续
int pthread_mutex_trylock(pthread_mutex_t *mutex);
功能:尝试对一个互斥锁加锁
mutex:尝试要上锁的互斥量
返回值:成功返回0,失败返回EBUSY(别线程再整加锁使用) 使用EBUSY加头文件errno.h
int pthread_mutex_unlock(pthread_mutex_t *mutex);
功能:对一个互斥锁进行解锁
mutex:要解锁的互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁一个互斥锁
3 信号量
与XSI机制中的信号量原理相似,相当于线程之间使用的计数器,用于控制访问有限的共享资源的线程数
sem_t是一种信号量数据类型,用于定义信号量变量
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,如果信号量值为0则等待一段时间,超时后返回ETIMEOUT
int sem_post(sem_t *sem);
功能:对信号量+1
int sem_destroy(sem_t *sem);
功能:销毁信号量
4 死锁
4.1 什么是死锁
多个进程/线程在使用共享资源时,相互等待对方手中的资源,在得到新资源前不会释放自己手上的资源,最终形成循环等待,这种现象称为死锁
4.2 产生死锁的4大必要条件
① 资源互斥:资源只有两种状态:可用和不可用状态,不能同时使用,同一时刻只能被一个进程或线程使用
② 占有且请求:已经得到了一些资源的进程或线程,继续请求新的资源,并持续占用旧资源
③ 资源不可剥夺:资源在分配给线程或进程后,不能被其它进程或线程强制性夺取,除非占用者主动放弃
④ 环路等待:当死锁发生时,系统中必定存在两个或两个以上的进程线程的执行路线形成环状
注意:死锁一旦形成,现在的操作系统无法解决已出现的思索情况,因此只能预防死锁的产生
4.3 预防死锁的产生:破坏死锁条件产生的其中一个条件
① 破坏资源互斥:
想办法让资源共享:共享或增加资源
缺点:受代码环境或者资金影响无法让资源共享
② 破坏占有且请求:
采用静态预分配机制,进程或线程可以再运行前一次性申请所有的资源,在资源没有得到全部满足前,不投入运行
缺点:系统资源会被严重浪费,因为有些资源可能开始时使用,有些资源结束时才使用
③ 破坏资源不可剥夺:
当一个进程或线程在占用了一个不可被剥夺的资源,并且请求新资源得不到满足时,就释放自己手中的所有资源,等待一段时间后,再重新申请资源
缺点:该策略实现难度较高,并且释放已或得的资源会导致前一阶段的工作全部失效,反复的申请释放资源会增大CPU的系统开销,浪费内存、时间。
④ 破坏环路等待:
给每个资源进行编号,进程或线程按照同一个编号顺序依次申请资源,并且只有拿到当前编号的资源后,才能去继续请求下一个编号的资源
缺点:资源数多,编号难度较高,并且受到实际使用资源顺序情况影响,当增加或删除资源时,又需要重新设计编号方案
4.4 如何判断死锁位置
画出整个程序的资源分配图
简化资源分配图
通过死锁原理:找出出现环路的位置
推荐使用:银行家算法
5 条件变量
通过条件变量,让线程满足某些条件时,可以自己进入睡眠,也可以在某些条件满足时被其他线程唤醒,一般配合互斥锁
pthread_cond_t 是一种条件变量数据类型,用于定义条件变量变量
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
功能:初始化一个条件变量
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_destroy(pthread_cond_t *cond);
功能:销毁条件变量
7 生产者消费者模型
生产者:产生数据的线程
消费者:使用数据的线程
仓库:临时存储数据的缓冲区,为了解决生产消费不协调的问题
可能产生的问题:
生产快于消费:仓库爆满,撑死
消费快于生产:仓库空虚,饿死
利用条件变量解决问题:
当仓库缓冲区满的时候,生产者线程睡入条件变量(full),通知消费者线程全部醒来(empty)
当仓库缓冲区空的时候,消费者线程睡入条件变量(empty),通知生产者线程全部醒来(full)
根据这个消费者生产者模型,我们可以设计出一个线程池,这是我已经封装好的线程池,有需要可以前往参考:
c语言——线程池https://download.csdn.net/download/snowysc/87774803