linux---线程安全(同步与互斥)

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;
}

理解上面的概念:

  • 临街资源:多线程执行流共享的资源就叫做临街资源,上面程序就是我们的num
  • 临界区:每个线程内部,访问临街自娱的代码,就叫做临界区
  • 原子性:不会被任何调度禁止打断的操作,该操作只有两种状态,要么完成要么未完成。
    我们判断线程是不是安全:判断在线程中是否对临界资源进行了非原子性的操作。
    2. 如何实现我们的线程安全
    实现我们的线程安全就使用同步与互斥,同步就是控制临界资源的合理访问(时序可控),互斥就是临界资源同一时间的唯一访问(我访问的时候别人不能去访问)。
    3. 什么是互斥和互斥的实现
    任何时刻,互斥保证有且只有一个执行流进入到临界区对临界资源进行操作,通常对临街资源起保护作用。
    通常来说线程的函数中处理的都是一些局部变量,如果在线程函数中处理了我们的全局变量或者static变量的话,在多个线程并发的时候就出现了数据的二义性,此时我们通常会采用互斥锁来解决问题,
    互斥锁:就是一个1/0计数器。1表示可以加锁,加锁就是计数-1,操作完之后进行解锁操作,解锁就是计数+1,0表示不可以加锁,不能加锁则等待,
    linux---线程安全(同步与互斥)_第1张图片
    实际上是从寄存器中映射到我们的内存中,是寄存器和我们的内存进行直接的交互,当我们的寄存器为0的时候,内存就变为0,则不能等待加锁。在大多数的体系结构都提供了swap和exchange指令,该指令的作用就是把寄存器和内存单元的数据进行交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先来后到,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。这就是我们互斥锁的实现。
    看下面一个代码(一个抢票程序)
  #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加锁,此时就会导致两个线程互相等待锁的释放,但是我们此时两个线程都出现等待。所以程序就会导致卡死。

  • 多线程环形锁
    linux---线程安全(同步与互斥)_第2张图片
    多个线程等待互相等待,线程1等待线程2释放锁,线程2等待线程3释放锁,线程3等待线程4释放锁,线程4等待线程1释放锁。从而导致哪一个都不会释放锁,导致程序卡死。
    避免死锁的条件:
  • 破坏必要条件(预防条件)
    避免死锁算法:
    - 银行家算法
    当一个进程申请使用资源的时候,银行家算法通过先试探分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态,若不处于安全状态的话试探分配作废,让该进程继续等待。
    linux---线程安全(同步与互斥)_第3张图片
    在银行家算法的进程:
  • 包含进程pi的需求资源数量(也是最大需求资源数量,MAX)
  • 已经分配的给该进程的资源A(Allocation)
  • 还需要的资源数量N(Need=M-A)

Available为空闲资源数量,即资源池,资源池的剩余资源数量+已经分配给所有进程的资源的数量=系统中的资源总量
假设资源P1申请资源,银行家算法先试探的分配给它(当然先要看看当前资源池中的资源数量够不够),若申请的资源数量小于等于Available,然后接着判断分配给P1后剩余的资源,能不能使进程队列的某个进程执行完毕,若没有进程可执行完毕,则系统处于不安全状态(即此时没有一个进程能够完成并释放资源,随时间推移,系统终将处于死锁状态)。
若有进程可执行完毕,则假设回收已分配给它的资源(剩余资源数量增加),把这个进程标记为可完成,并继续判断队列中的其它进程,若所有进程都可执行完毕,则系统处于安全状态,并根据可完成进程的分配顺序生成安全序列(如{P0,P3,P2,P1}表示将申请后的剩余资源Work先分配给P0–>回收(Work+已分配给P0的A0=Work)–>分配给P3–>回收(Work+A3=Work)–>分配给P2–>······满足所有进程)。

  1. 什么是同步和同步的实现
    在数据安全保证的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题。
    同步其实是对临界资源访问合理性的问题,就是我们生产了才能使用,没有资源就一直等待,生产了资源后就唤醒等待。
    通常使用条件变量对线程进行我们的同步操作,条件变量就是当一个线程互斥地访问某一个变量的时候,他可能发现在其他线程改变状态之前,它什么也做不了。
    条件变量接口
  • 条件变量初始化
       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. 可重入和不可重入函数
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。
可重入和不可重入的区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的
    可重入和不可重入的线程的安全
    可重入是线程安全的,而不可重入不一定是线程不安全的,在没有对全局或者静态的变量进行我们的操作的时候可能是安全的,需要在具体的场景下进行判断。
    不可重入函数例子:
  • malloc函数
  • 调用标准I/O库函数
    常见可重入情况
  • 不使用全局变量或静态变量 -
  • 不使用用malloc或者new开辟出的空间 -
  • 不调用不可重入函数 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

你可能感兴趣的:(linux操作系统)