上一节我讲了Linux线程的一些基础知识,返现多线程是不安全的,会引发安全问题,所以今天就来讲一下Linux线程安全一节的内容!
1.线程安全概念
线程安全:多个线程同时操作临界资源不会出现数据二义性
2.线程安全的实现
这里要引入两个概念:同步与互斥
同步:临界资源访问的时序可控
互斥:临界资源访同一时间的唯一访问性
临界资源:多线程执行流共享的资源
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
可重入/不可重入:多个执行流中是否可以同时进入函数运行而不会出现问题,是否在线程函数中对临界资源进行了非原子操作。
下面来举个例子:(为了举例,当然这是一种理想情况,不可能不存在竞争)
一个系统中多个线程必然要共享一些系统资源,如共享CPU,共享I/O设备。
以打印机为例,线程A在使用打印机时,其他线程不能使用而只能等待,这就是互斥,保证临界资源(打印机)的同一时间的唯一访问性。
线程A用完了,它后面的线程再逐个接着使用,这就是同步,保证临界资源访问的时序可控。
在访问临界资源时会产生资源竞争问题,这就需要实现互斥,那么如何实现互斥呢?
这就需要用到互斥锁(mutex):
互斥锁原理图:
上图中就用到了互斥锁变量mutex,当一个线程访问临界资源时就加锁,其他线程只能等待,无法访问,等到该线程访问完毕解锁后,其他线程才能继续访问。
互斥锁的操作步骤:(用到的接口)
1.定义互斥锁变量
pthread_mutex_t mutex
2.初始化互斥锁变量
pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
mutex: 要初始化的互斥量
attr:NULL
3.加锁/解锁
pthread_mutex_lock(pthread_mutex_t *mutex);
阻塞加锁:加不上锁就阻塞
pthread_mutex_trylock(pthread_mutex_t *mutex);
非阻塞加锁:加不上锁就报错返回
pthread_mutex_timelock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout)
限时阻塞加锁
pthread_mutex_unlock(pthread_mutex_t *mutex);
解锁
返回值:0 失败:错误号
4.销毁互斥锁
pthread_mutex_destroy(pthread_mutex_t *mutex);
加了锁也不是一定就不会出问题,比如会出现死锁情况如下图!!!
死锁:因为对一些无法加锁的锁,进行加锁而导致程序卡死
产生:对资源的竞争以及进程/线程加锁的推进顺序不当
死锁产生的四个必要条件:
1.互斥操作(我操作的时候别人不能操作)
2.不可剥夺条件(我加的锁.别人不能解)
3.请求与保持条件(拿着手里的,请求其他的,其他的请求不到,手里的也不放)
4.环路等待条件
产生场景:加锁/解锁顺序不同
预防死锁:破坏必要条件
避免死锁:死锁检测算法,银行家算法
银行家算法:
这个算法为什么叫银行家算法呢?是因为这个算法同样适用于银行的贷款业务。
当一个进程申请使用资源的时候,银行家算法通过先试探分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态,若不安全则试探分配作废,让该进程继续等待。< font>
最后通过安全判定算法通过的序列就称为安全序列,这个安全序列就不会产生类似死锁的情况。
假设有进程P1,P2,,,,Pn
则安全序列应该满足:Pi(0<i<n)需要的资源 = 分配给Pj(0<j<i)的资源 + 剩余资源< font>
为什么还要加上分配给Pj的资源呢?你想想银行贷款他总不可能只出不进吧?别人贷了款总要还款的啊。
int n,m //系统中的进程数n和资源数m
int Avaliable[1..m] //资源当前可用总量
int Allocation[1...n,1...m] //当前分配给各个进程的各种资源数
int Need[1...n,1...m] //当前每个进程还需分配的各种资源数量
int Work[1...m] //当前可分配的资源
bool Finish[1...n] //进程是否完成
银行家算法->安全判定算法
1.初始化
Work = Available (动态记录当前剩余资源)
Finish[i] = false (设当前所有进程均未完成)
2.查找可执行的Pi (未完成的话但是剩余资源能满足其需要,这样也算可以完成)
Finish[i] = false
Need[i]<=Work
如果没有这样的进程Pi,就跳转到第4步
3.如果没有 Pi一定可以完成,并归还分配给其的资源
Finish[i] = true
Work = Work + Allocation[i]
继续返回第2步查找
4.如果所有进程Pi都能完成,即Finish[i] = true
则系统处于安全状态,否则是不安全状态
实现伪代码:
Boolean Found;
Work = Available ;
Finish[1..n] = false;
while(true){
Found = false;
for(int i=0;i<n;i++){
if(Finish[i]==false&&Need[i]<=Work){
Work = Work + Allocation[i];
Finish[i] = true;
Found = true;
}
}
if(Found == false){
break;
}
}
for(int i=0;i<n;i++){
if(Finish[i]==false){
return "Unfinished"
}
}
同步的实现:
临界资源的访问合理性—生产出来才能使用,没有资源则等待(死等),生产资源后唤醒等待。
条件变量:例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情 况就需要用到条件变量。
1.定义条件变量
pthread_cond_t n
2.初始化条件变量
pthread_cond_init(&n)
3.等待/唤醒
pthread_cond_wait(&n,&mutex)
pthread_cond_signal(&n)
4.销毁条件变量
pthread_cond_destroy(&n)
条件变量为什么要搭配互斥锁使用?
因为条件变量本身只提供等待与唤醒功能,具体什么时候等待需要用户来进行判断,这个条件的判断,通常涉及到临界资源的操作(其他线程要通过修改条件,来促使条件满足)而这个临界资源的操作应该收保护,因此搭配互斥锁一起使用。< font>
最后给大家举个例子:(利用同步与互斥)
实现了一个简单的黄牛抢票程序
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<pthread.h>
5
6 int ticket = 100;
7 pthread_mutex_t mutex;
8
9 void* pth_start(void* arg){//pthread_create 入口函数
10 while(1){
11 pthread_mutex_lock(&mutex);//在访问临界资源前加互斥锁
12 if(ticket>0){
13 printf("第 %d 黄牛,抢了张票: %d\n",(int)arg,ticket);
14 ticket--;
15 }
16 else{
17 pthread_mutex_unlock(&mutex);//如果没票了也要解锁,否为后面将会死等
18 pthread_exit(NULL);
19 }
20 pthread_mutex_unlock(&mutex);//一个黄牛抢到票后要解锁,其他的黄牛可以抢票
21 }
22 return NULL;
23 }
24
25 int main(int argc,char* argv[])
26 {
27 pthread_t tid[4];
28 int i=0,ret;
29 //互斥锁初始化
30 pthread_mutex_init(&mutex,NULL);
31 //创建四个线程来充当黄牛抢票
32 for(;i<4;i++){
33 ret = pthread_create(&tid[i],NULL,pth_start,(void*)i);
34 if(ret!=0){
35 printf("yellowbull not exist!");
36 return -1;
37 }
38 }
39 //退出线程
40 for(i=0;i<4;i++){
41 pthread_join(tid[i],NULL);
42 }
43 //销毁互斥锁变量
44 pthread_mutex_destroy(&mutex);
45 return 0;
46 }
在这里就用到了互斥锁变量,如果不加互斥锁的话,会出现多个黄牛抢了同一张票,这显然是不合理的,所以在访问临界资源时要加锁,这样每次就是一个黄牛在访问临界资源(抢票),在访问完毕后要解锁,如果没有加完锁后发现没票了,也是要解锁的,否则会导致后面的黄牛死等。