线程互斥
我们之前介绍过互斥的概念,但没有介绍Linux中线程互斥的操作
互斥存在的必要性是因为访问共享资源时,有可能被CPU换下,这样就会产生bug
例如说我们上一篇中说过的抢票,我们虽然设置的是当ticket小于等于0时结束线程
但是当五个线程几乎同时进入这个判断,就会导致过量运行
主要是因为这些操作并非原子的,不是原语
要解决这个问题就需要互斥锁
互斥锁的使用
互斥锁的使用一般分为四个步骤
- 初始化互斥锁
- 临界区前加锁
- 临界区后解锁
- 用完后销毁
初始化互斥锁
这里有两个方法,第一个是静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
第二个是动态分配
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr)
第一个是要初始化的互斥量,我们在调用之前声明一共传入即可,第二个我们传入NULL
加锁和解锁
int pthread_mutex_lock(pthread_mutex_t* mutex)
int pthread_mutex_unlock(pthread_mutex_t* mutex)
当原来的互斥量没有被加锁,此时加锁就会成功
当互斥量已经被上锁了,这个线程就会被阻塞,等待互斥量解锁
销毁互斥锁
静态的互斥锁不需要销毁
已经加锁的互斥量不允许被销毁
已经销毁的互斥量,要确保以后不会再被用到
int pthread_mutex_destory(pthread_mutex_t* mutex)
这里互斥锁的库要直接使用也是不方便的,我们同样可以给其封装成类会好用很多
线程死锁
死锁指的是一个线程或者几个线程互相阻塞等待,没有任何一个线程在运行
要避免陷入死锁可以尝试破坏以下几个条件
- 互斥条件
- 请求与保持:一个执行流被阻塞时依然占据其保持的其他资源
- 不剥夺条件:一个执行流获得的资源在未使用完之前不能被强行剥夺
- 循环等待条件
线程安全与可重入
线程安全指的是在多个线程并发执行同一段代码时,不会出现不同的结果
这里主要指的是对于全局变量和静态变量
重入指的是,同一个函数被不同的执行流调用时,当一个执行流还没有执行完成,另一个执行流就再次进入的情况。一个函数可重入就说明可以同时搭载多个执行流而不出现bug,否则就是不可重入函数
实际上可重入的条件比线程安全更加严格
下面的情况出现,就说这个函数不可重入,否则会出现bug
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 标准IO库函数,标准IO库的很多实现都已不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
一般来说,函数可重入,那么线程一定是安全的,如果函数不可重入,那么就不允许多线程并发使用
线程同步
一方面是因为有可能资源被一个线程霸占,另一方面我们可能希望执行流有先后的执行顺序
同步就是让线程的执行流有先后的执行顺序
竞态条件指的是,因为时序问题,导致的程序问题
线程同步的接口
与互斥类似,线程同步也分为四部
- 初始化条件变量
- 利用条件变量等待资源就绪
- 资源就绪后唤醒线程来访问
- 使用后销毁条件变量
初始化条件变量
pthread_cond_t cond;//定义变量后再初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
cond是要初始化的条件变量
attr传入NULL
利用条件变量等待资源就绪
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex)
要在cond这个条件变量上等待,mutex是互斥量
进程资源等待的前提是,拿到锁进入临界区,所以wait函数需要条件变量和互斥锁
唤醒等待
int pthread_cond_broadcast(pthread_cond_t* cond);
int pthread_cond_signal(pthread_cond_t* cond);
第一个是唤醒所有线程,是广播
第二个是唤醒某一个线程
销毁
int pthread_cond_destory(pthread_cond_t* cond);