1. 什么是线程安全
2. 如何实现线程安全
3. 什么是互斥和互斥的实现
4. 死锁
5. 什么是同步和同步的实现
6. 可重入和不可重入函数
1. 什么是线程安全
多个线程同时操作临界资源,而不会出现数据的二义性就说明这个线程就是线程安全。比如看下面的例子,当我们调用下面的例子的时候会出现不一样的结果,因为在线程中我们对num的操作是一个非原子性操作,在这个里面我们的理想的结果是输出5,在我们调用程序的时候可能会出现7或者10等等的结果,是因为我们开辟两个线程都同时对这个num进行了操作。
#include
#include
#include
int num = 0;
void* thr_start(void* arg){
num+=2;
sleep(5);
num+=3;
printf("%d\n",num);
}
int main(){
int a = 0;
int ptid[2];
for(int i = 0;i < 2; i++){
pthread_create(&ptid[i],NULL,th_start,NULL);
pthread_detach(ptid[i]);
}
sleep(10);
return 0;
}
理解上面的概念:
#include
#include
#include
#include
int num = 100;
pthread_mutex_t mutex;
void* thr_a(void* arg){
while(1){
if(num > 0){
printf("----%d---抢到了%d号票\n",(int)arg,--num);
}else{
return NULL;
}
}
return NULL;
}
int main(){
pthread_t tid[4];
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);
}
return 0;
}
此时会出现结果是一张票会多次被抢,此时使用我们的互斥锁进行处理
互斥锁接口:
pthraed_mutex_t _mutex;
#include
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,不成功返回errno
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);
销毁互斥锁
修改上述程序解决互斥的问题
1 #include
2 #include
3 #include
4 #include
5 int num = 100;
6 pthread_mutex_t mutex;
7 void* thr_a(void* arg){
8 while(1){
9 pthread_mutex_lock(&mutex);
10 if(num > 0){
11 printf("----%d---抢到了%d号票\n",(int)arg,--num);
12 }else{
13 pthread_mutex_unlock(&mutex);
14 return NULL;
15 }
16 pthread_mutex_unlock(&mutex);
17 }
18 return NULL;
19 }
20 int main(){
21 pthread_t tid[4];
22 pthread_mutex_init(&mutex,NULL);
23 int i = 0;
24 for(; i < 4; i++){
25 pthread_create(&tid[i],NULL,thr_a,(void*)i);
26 }
27 for(i = 0;i < 4; ++i){
28 pthread_join(tid[i],NULL);
29 }
30 pthread_mutex_destroy(&mutex);
31 return 0;
32 }
4. 死锁
死锁:死锁就是因为在加锁之后诶呦进行解锁而导致程序卡死(对一些无法加锁的锁进行加锁而导致程序卡死)
死锁产生的4个必要条件
void data_process()
{
EnterCriticalSection();
if(/* error happens, forget LeaveCriticalSection */)
return;
LeaveCriticalSection();
}
当我们不释放锁的时候别人加锁的时候就会一直等待,从而出现了死锁的情况,导致别人就一直不能加锁,程序卡死
void sub_func()
{
EnterCriticalSection();
do_something();
LeaveCriticalSection();
}
void data_process()
{
EnterCriticalSection();
sub_func();
LeaveCriticalSection();
}
单线程重复加锁的时候是因为我们单线程申请一个锁之后我们没有释放的时候又进行加锁操作,此时我们上一个锁没有进行释放,此时我们加锁就加锁不上,就会一直出现等待的情况,所以出现程序卡死
void data_process1()
{
EnterCriticalSection(&cs1); // 申请锁的顺序有依赖
EnterCriticalSection(&cs2);
do_something1();
LeaveCriticalSection(&cs2);
LeaveCriticalSection(&cs1);
}
void data_process2()
{
EnterCriticalSection(&cs2); // 申请锁的顺序有依赖
EnterCriticalSection(&cs1);
do_something2();
LeaveCriticalSection(&cs1);
LeaveCriticalSection(&cs2);
}
多线程申请多锁的时候对顺序有依赖,当我们两个线程对cs1和cs2锁分别进行加锁的时候,当我们队线程1对cs1加锁成功,线程2对cs2加锁成功的话我们线程1就不能对cs2加锁。线程2不能对cs1加锁,此时就会导致两个线程互相等待锁的释放,但是我们此时两个线程都出现等待。所以程序就会导致卡死。
Available为空闲资源数量,即资源池,资源池的剩余资源数量+已经分配给所有进程的资源的数量=系统中的资源总量
假设资源P1申请资源,银行家算法先试探的分配给它(当然先要看看当前资源池中的资源数量够不够),若申请的资源数量小于等于Available,然后接着判断分配给P1后剩余的资源,能不能使进程队列的某个进程执行完毕,若没有进程可执行完毕,则系统处于不安全状态(即此时没有一个进程能够完成并释放资源,随时间推移,系统终将处于死锁状态)。
若有进程可执行完毕,则假设回收已分配给它的资源(剩余资源数量增加),把这个进程标记为可完成,并继续判断队列中的其它进程,若所有进程都可执行完毕,则系统处于安全状态,并根据可完成进程的分配顺序生成安全序列(如{P0,P3,P2,P1}表示将申请后的剩余资源Work先分配给P0–>回收(Work+已分配给P0的A0=Work)–>分配给P3–>回收(Work+A3=Work)–>分配给P2–>······满足所有进程)。
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
int pthread_cond_destroy(pthread_cond_t *cond);
cond:就是定义的条件变量的变量
返回值,成功返回0,失败返回-1.
#include
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
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast():唤醒所有人
pthread_cond_signal():唤醒至少一个人
根据我们同步与互斥就可以写出我们的生产者和消费者模型,下节见真章。
6. 可重入和不可重入函数
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。
可重入和不可重入的区别