临界资源:
多线程执行流共享的资源就叫做临界资源
临界区:
每个线程内部,访问临界资源的代码,就叫做临界区
互斥:
任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源
原子性:
不会被任何调度机制打断的操作,该操作只有两态,完成和未完成
可重入:
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果仍正确,则该函数被称为可重入函数,否则,是不可重入函数。
死锁:
死锁是一种永久等待的状态,产生的原因是线程/线程互相申请被其他线程占用且不释放的资源
产生死锁的四个必要条件:
互斥:
一个资源每次只能被一个执行流使用
请求与保持条件:
一个执行流因请求资源而阻塞时,对已获得的资源保持占有
不剥夺条件:
一个执行流已获得的资源不能被强行剥夺
循环等待条件:
几个执行流之间形成一种头尾相接的循环等待资源的情况
死锁的解决方法:
解决死锁的方法可以从多个角度去分析,一般的情况下,有预防,避免,检测和解除四种。
饥饿:
如果一个线程因为处理器时间全部被其他线程抢走而得不到处理器运行时间,这种状态被称之为饥饿,一般是由高优先级线程吞噬所有的低优先级线程的处理器时间引起的
同步:
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
线程安全:多个线程并发执行时,不管线程的调度顺序如何,程序结果都是正确的。
线程安全问题常出现在没有锁保护的情况下对全局变量或者静态变量进行操作
有些变量需要在线程间共享,这样的变量称为共享变量。通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,需要满足:
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量,通过lock和unlock达到上述目的。
加锁实现原理:
互斥量默认为1,执行xchgb后,只有一个线程寄存器的值为1,可以进入临界区。
互斥量此时为0,其他线程寄存器的值为0,即使其他线程寄存器和互斥量交换数值,寄存器里的值也是0,无法进入临界区。
线程寄存器数值为1的线程执行对应的代码后执行goto lock,线程寄存器的值置为0。
释放锁实现原理:
互斥量置1即可,其他线程可通过执行xchgb语句将线程寄存器的值置1后,进入临界区。
初始化互斥量
静态初始化:
如果互斥锁 mutex 是静态分配的(定义在全局,或加static关键字修饰),可以直接用宏来初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
动态初始化:
int pthread_mutex_init(&mutex, NULL); //mutex为定义的互斥量
销毁互斥量
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要手动销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
互斥量加锁解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
加锁时的情景:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么
pthread_ lock会陷入阻塞(执行流被挂起),等待互斥量解锁。
条件变量:
当一个线程的行为依赖于另外一个线程对共享数据状态的改变时,这时候就可以使用条件变量。
条件变量的两个动作:
1. 条件不满足: 阻塞线程
2. 条件满足: 通知阻塞的线程开始工作
例如:
对于一个生产者消费者问题,如果没有产品而消费线程使用不断查询来判断是否有产品将大量的浪费CPU时间。如果没有产品时消费者线程可以先阻塞,等待有产品时被唤醒进行消费。
初始化条件变量:
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);
参数:
cond:要在这个条件变量上等待
mutex:互斥量
这里参数传入互斥量是因为,如果lock后进行等待,还没有unlock,其他线程无法获取互斥量,会造成死锁。传入互斥量后,等待时自动释放互斥量,被唤醒时自动获取互斥量。
唤醒
int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒所有该条件变量下挂起的线程
int pthread_cond_signal(pthread_cond_t *cond); //唤醒一个该条件变量下挂起的线程
在线程被唤醒过程中,如果锁被其他线程抢占执行,等持锁线程执行完后,被唤醒线程获得锁执行,就有可能造成临界资源被过度消费为负数的现象(在生产者消费者模式中)。
避免伪唤醒:
当我们在线程唤醒后,再加一次对临界资源的判断,就能有效规避此问题,所以关于临界资源的判断语句不使用if而使用while
如果信号量是一个任意的整数,通常被称为计数信号量(一般信号量)
如果信号量只有二进制的0或1,称为二进制信号量,在linux系统中,二进制信号量又称互斥锁
信号量S代表临界资源的个数
信号量存在两种操作,V操作与P操作,V操作会增加信号量 S的数值,P操作会减少它。
具体的运作方式如下:
初始化,给与它一个非负数的整数值。
运行 P(wait()),信号量S的值将被减少。企图进入临界区的进程,需要先执行P操作。当信号量S减为负值时,进程会被挡住,不能继续;当信号量S不为负值时,进程可以获准进入临界区。
运行 V(signal()),信号量S的值会被增加。即将离开临界区的进程,将会执行V操作。当信号量S不为负值时,先前被挡住的其他进程,将可获准进入临界区。
此部分参自🔗
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待(P)
int sem_wait(sem_t *sem);
功能:会将信号量的值减1
唤醒(V)
int sem_post(sem_t *sem);
功能:表示资源使用完毕,可以归还资源了,将信号量值加1。