本文重点:
1.线程安全概念:
2.线程安全的实现方法:
3.线程间同步的实现:
4.线程间互斥的实现:
5.死锁的产生以及预防:
6.可重入与不可重入的实现:
一.什么是线程安全??
多个线程同时操作临界资源,而不会出现数据的二义性就说明这个线程就是线程安全;
临界资源:多线程执行流共享的资源就叫做临街资源;
临界区:每个线程内部,访问临界资源的代码为临界区;
原子性:不会被任何调度禁止打断的操作,该操作只有两种状态,要么完成要么未完成;
我们判断线程是不是安全:判断在线程中是否对临界资源进行了非原子性的操作。
二.如何实现线程安全??
实现我们的线程安全就使用同步与互斥,同步就是控制临界资源的合理访问(时序可控),互斥就是临界资源同一时间的唯一访问(我访问的时候别人不能去访问);
三.线程间互斥的实现:互斥锁
任何时刻,互斥保证有且只有一个执行流进入到临界区对临界资源进行操作,通常对临街资源起保护作用。
通常来说线程的函数中处理的都是一些局部变量,如果在线程函数中处理了我们的全局变量或者static变量的话,在多个线程并发的时候就出现了数据的二义性,此时我们通常会采用互斥锁来解决问题;
互斥锁:0/1的原子计数器+等待对列;1表示可以加锁,加锁就是计数-1,操作完之后进行解锁操作,解锁就是计数+1,0表示不可以加锁,不能加锁则等待;
实际上是从寄存器中映射到我们的内存中,是寄存器和我们的内存进行直接的交互,当我们的寄存器为0的时候,内存就变为0,则不能等待加锁。在大多数的体系结构都提供了swap和exchange指令,该指令的作用就是把寄存器和内存单元的数据进行交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先来后到,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。这就是我们互斥锁的实现。
1.互斥锁的实现流程:
- 定义互斥锁变量:
pthraed_mutex_t _mutex;
- 对互斥锁变量进行初始化:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
初始化方式有两种,一种是静态初始化,在定义的时候就进行初始化。一种是函数初始化。互斥锁变量一定要使用此锁的线程都能访问;
参数:
mutex:是定义的互斥锁变量
attr:互斥锁的属性,一般置为NULL。
返回值:成功返回0;不成功返回erron;
- 加锁,解锁操作:
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
加锁:在临界资源操作之前,要在线程中任意有可能退出的地方进行加锁。
参数:mutex,定义是的互斥锁;
int pthread_mutex_trylock(pthread_mutex_t *mutex);是尝试加锁,如果不成功就立即放回。
int pthread_mutex_lock(pthread_mutex_t *mutex);加锁,不能加锁则等待
int pthread_mutex_unlock(pthread_mutex_t *mutex);解锁操作
- 销毁互斥锁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
2.实现互斥的代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int num = 100;
pthread_mutex_t mutex;
void* thr_a(void* arg){
while(1){
pthread_mutex_lock(&mutex);
if(num > 0){
printf("----%d---抢到了%d号票\n",(int)arg,--num);
}else{
pthread_mutex_unlock(&mutex);
return NULL;
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
pthread_t tid[4];
pthread_mutex_init(&mutex,NULL);
int i = 0;
for(; i < 4; i++){
pthread_create(&tid[i],NULL,thr_a,(void*)i);
}
for(i = 0;i < 4; ++i){
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}
四.死锁:
1.什么是死锁??
死锁就是因为在加锁之后诶呦进行解锁而导致程序卡死(对一些无法加锁的锁进行加锁而导致程序卡死;
2.死锁产生的4个必要条件:
- (1)互斥条件一个锁只有一个人可以获取;
- (2)不可剥夺条件:我加的锁别人不能解;
- (3)请求与保持条件:拿着A锁去请求B锁,但是获取不到B锁,也不释放A锁;
- (4)环路等待条件:我拿着A锁请求B锁,对方拿着B锁请求A锁;
四个条件必须同时具备才会产生死锁;
3.死锁产生的场景:
- 忘记释放锁: 当我们不释放锁的时候别人加锁的时候就会一直等待,从而出现了死锁的情况,导致别人就一直不能加锁,程序卡死;
- 单线程重复申请锁: 单线程重复加锁的时候是因为我们单线程申请一个锁之后我们没有释放的时候又进行加锁操作,此时我们上一个锁没有进行释放,此时我们加锁就加锁不上,就会一直出现等待的情况,所以出现程序卡死;
- 多线程多锁申请: 多线程申请多锁的时候对顺序有依赖,当我们两个线程对cs1和cs2锁分别进行加锁的时候,当我们队线程1对cs1加锁成功,线程2对cs2加锁成功的话我们线程1就不能对cs2加锁。线程2不能对cs1加锁,此时就会导致两个线程互相等待锁的释放,但是我们此时两个线程都出现等待。所以程序就会导致卡死;
- 多线程环形锁: 多个线程等待互相等待,线程1等待线程2释放锁,线程2等待线程3释放锁,线程3等待线程4释放锁,线程4等待线程1释放锁。从而导致哪一个都不会释放锁,导致程序卡死。
4.死锁的预防: 破坏四个必要条件;
5.死锁的避免:
- 死锁检测算法:
- 银行家算法:
五.线程同步的实现:等待与唤醒;
1.如何实现线程的同步:
条件变量:等待+唤醒+等待对列(用于实现线程间同步)
注意:条件变量只实现了等待与唤醒的功能,但是具体什么时候该等待,什么时候该唤醒,需要由用户自身做判断;
条件变量实现同步:线程在对临界资源访问之前,先判断是否能够进行操作;若可以进行操作则线程直接操作;否则若不能进行操作;则条件变量提供等待功能;让pcb等待在队列上;其他线程促使条件满足,然后唤醒条件变量等待对列上的线程;条件变量通过提供线程等待与唤醒线程的实现线程同步;条件变量本身不具备条件判断的功能;也就意味着什么时候氙灯该等待,什么时候该唤醒的线程,都需要用户来自己控制;
2.实现线程同步的接口:
- 条件变量的初始化:
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
一个是静态初始化,在定义条件变量的时候就直接初始化,一个是函数初始化,和我们的互斥锁类似。
参数
cond:定义的条件变量的变量
attr:条件变量的属性
返回值:成功返回0,失败返回errno
- 条件变量的销毁:
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
cond:就是定义的条件变量的变量
返回值,成功返回0,失败返回-1.
- 等待:
#include <pthread.h>
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
分为了定时等待和永久等待。定时等待就是在设定的时间之内进行等待,如果超出时间就报错返回,永久等待就一直等待下去,直到有人唤醒操作;
wait操作不只是简单的等待的操作,包含了解锁后挂起的操作,其实是完成了三个操作:
1.解锁操作;
2.休眠,挂起操作;
3. 被唤醒后加锁操作;
为什么等待操作需要搭配锁的使用??
因为条件变量本身只提供等待与唤醒的功能,具体什么时候等待需要用户来进行判断,这个条件判断,通常涉及到我们队临界资源的操作(其他线程要通过修改条件来促使条件满足),而这个临界资源应该受保护,所以我们此时需要搭配锁的使用;
- 唤醒等待:
#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast():唤醒所有人;
pthread_cond_signal():唤醒至少一个人;
六.可重入和不可重入函数:
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。
可重入和不可重入的区别:
- 可重入函数是线程安全函数的一种;
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。;
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的;
- 可重入和不可重入的线程的安全:
可重入是线程安全的,而不可重入不一定是线程不安全的,在没有对全局或者静态的变量进行我们的操作的时候可能是安全的,需要在具体的场景下进行判断;
不可重入函数例子:
- malloc函数;
- 调用标准I/O库函数
可重入的情况:
- 不使用全局变量或静态变量 ;
- 不使用用malloc或者new开辟出的空间 -;
- 不调用不可重入函数 不返回静态或全局数据,所有数据都有函数的调用者提供;
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据;