Linux---线程的同步和互斥

目录

1、线程的互斥

2、可重入VS线程安全

3、线程的同步


1、线程的互斥

1)线程互斥的相关概念

  • 临界资源:被多个执行流共享的资源就称为临界资源,例如全局变量。
  • 临界区:访问临界资源的代码称为临界区。
  • 互斥:互斥保证了任何时刻只有一个线程进入临界区访问临界资源。
  • 原子性:不会被任何机制打断的操作,该操作只有两态,要么已经完成要么还没开始(不能存在已经开始了,但是还没完成的情况,简单理解就是依据汇编代码就可以实现的)。

Linux---线程的同步和互斥_第1张图片

2)通过订票示例引入互斥量

#include    
#include    
#include    
      
int tickets = 1000;//票数,每订购一张票数减1    

void* ticket(void* arg)    
{    
    //模拟订票过程,多个线程(执行流)访问该程序    
    while(1)    
    {    
      if(tickets > 0)    
      {    
          usleep(500);                                                                                                                                                                       
          printf("%s,抢票成功,剩余票数:%d\n",(char*)arg,--tickets);                                                                                                       
      }                                                                                                                                                                   
      else                                                                                                                                                                
          break;                                                                                                                                                            
    }                                                                                                                                                                     }                                                                                                                                                                       
                                                                                                                                                                          
int main()                                                                                                                                                              
{                                                                                                                                                                       
    //创建多个线程                                                                                                                                                        
    pthread_t tid[5];                                                                                                                                                     
    pthread_create(&tid[0],NULL,ticket,"new thread1");                                                                                                                    
    pthread_create(&tid[1],NULL,ticket,"new thread2");                                                                                                                    
    pthread_create(&tid[2],NULL,ticket,"new thread3");                                                                                                                    
    pthread_create(&tid[3],NULL,ticket,"new thread4");                                                                                                                    
    pthread_create(&tid[4],NULL,ticket,"new thread5");                                                                                                                    
                                                                                                                                                                          
    //线程等待                                                                                                                                                            
    pthread_join(tid[0],NULL);                                                                                                                                            
    pthread_join(tid[1],NULL);                                                                                                                                            
    pthread_join(tid[2],NULL);                                                                                                                                            
    pthread_join(tid[3],NULL);                                                                                                                                            
    pthread_join(tid[4],NULL);                                                                                                                                            
    return 0;                                                                                                                                                             
}                                 

运行结果分析:

Linux---线程的同步和互斥_第2张图片

第一个原因:当一个执行流执行到if判断为真以后,首先会usleep(500),在这期间ticket还没有进行--操作,因此ticket还是大于0的,如果这时再有其他执行流执行if还是判断为真,然后等待一段时间多个执行流对ticket(此时值为0)进行--就变成了负值。

Linux---线程的同步和互斥_第3张图片

第二个原因:ticket--本身就不是原子性的,汇编代码如下:

Linux---线程的同步和互斥_第4张图片

如何解决这个问题?

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区执行
  • 如果多个线程同时要进入临界区执行,且当前临界区没有线程在执行时,只允许一个线程进入该临界区进行执行。
  • 如果线程不在临界区执行,或者已经从临界区执行完了,则不能组织其他线程进入临界区执行。

满足这些条件的实际上就是一把锁,Linux中提供的锁叫做互斥量。注意:要保证加锁后其他线程不能进入临界区,则其他线程必须能够看到锁,也就是说锁也是临界资源(多个线程都可以访问),因此锁在保证临界区前必须先保证自己的安全,也就是说加锁的过程必须是原子性的(保证了多个线程进入该临界区时只允许一个进入)。

Linux---线程的同步和互斥_第5张图片

3)互斥量的相关接口

初始化互斥量

方法1:静态分配(全局变量)

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;

方法2:动态分配

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

         参数:mutex需要初始化的互斥量    attr暂不关注,置NULL即可

         返回值:成功返回0,失败返回错误码

注意:如果使用方法1初始化的互斥量不需要销毁

销毁互斥量

int  pthread_mutex_destroy(pthread_mutex_t *restrict mutex);

注意:不要销毁一个已经加锁的互斥量;已经销毁的互斥量,要确保后面不会在尝试进行加锁。

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

使用互斥量改进售票系统代码

#include    
#include    
#include    
      
pthread_mutex_t mutex;//初始化互斥量    
int tickets = 10000;//票数,每订购一张票数减1                                                                                                                                            
      
void* ticket(void* arg)    
{    
    //模拟订票过程,多个线程(执行流)访问该程序    
    while(1)    
    {    
      pthread_mutex_lock(&mutex);//加锁    
      if(tickets > 0)    
      {    
        printf("%s,抢票成功,剩余票数:%d\n",(char*)arg,--tickets);    
        pthread_mutex_unlock(&mutex);//解锁    
        usleep(500);    
      }    
      else    
      {    
        pthread_mutex_unlock(&mutex);    
        break;    
      }    
    }    
}    
      
int main()    
{    
    //创建多个线程    
    pthread_t tid[5];    
    pthread_create(&tid[0],NULL,ticket,"new thread1");    
    pthread_create(&tid[1],NULL,ticket,"new thread2");    
    pthread_create(&tid[2],NULL,ticket,"new thread3");    
    pthread_create(&tid[3],NULL,ticket,"new thread4");    
    pthread_create(&tid[4],NULL,ticket,"new thread5");    
      
    //线程等待    
    pthread_join(tid[0],NULL);    
    pthread_join(tid[1],NULL);
    pthread_join(tid[2],NULL);
    pthread_join(tid[3],NULL);
    pthread_join(tid[4],NULL);
  
    //销毁互斥量
    pthread_mutex_destroy(&mutex);
    return 0;
}                           

4)互斥量原理

Linux---线程的同步和互斥_第6张图片

语言层面单纯的i++/++i等并不是具有原子性,可能会有数据一致性问题。为了实现互斥锁操作,大多数体系结构都实现了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相互交换,由于只有一跳指令,保证了原子性。

Linux---线程的同步和互斥_第7张图片

2、可重入VS线程安全

1)相关概念

  • 线程安全:多个线程并发执行同一段代码不会出现不同结果。常见对全局变量或静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 可重入:同一个函数被不同的执行流执行,当一个执行流还没有结束,其他执行流就再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或任何问题的情况下,我们称之为可重入。反之,称之为不可重入。

2)常见线程不安全的情况

  • 不保护共享变量的函数

  • 函数状态随着被调用,状态发生变化的函数

  • 返回指向静态变量指针的函数

  • 调用线程不安全函数的函数

3)常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

  • 类或者接口对于线程来说都是原子操作

  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

4)常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

  • 可重入函数体内使用了静态的数据结构

5)常见可重入的情况

  • 不使用全局变量或静态变量

  • 不使用用malloc或者new开辟出的空间

  • 不调用不可重入函数

  • 不返回静态或全局数据,所有数据都有函数的调用者提供

  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

6)可重入与线程安全的区别与联系

  • 函数是可重入的,那就是线程安全的

  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

  • 可重入函数是线程安全函数的一种

  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的

3、线程的同步

1)常见锁的概念

死锁是指在在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程占用的不会释放的资源而处于一种永久等待的状态。

死锁的四个必要条件

  • 互斥条件:一个资源一次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而等待时,对已申请到的资源保持不放。
  • 不剥夺条件:一个执行流已获得的资源,在未使用完之前不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

死锁的避免

  • 破坏死锁的四个条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

死锁的避免算法

  • 四所检测算法
  • 银行家算法

3)Linux线程同步相关概念

  • 条件变量:当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
  • 同步概念:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

4)什么是线程同步?为什么要存在同步?

在保证线程安全的条件下,让多个执行流按照特定的顺序访问临界资源,我们称之为同步。

Linux---线程的同步和互斥_第8张图片

线程同步保证了多个线程协同完成任务的安全性和高效性。

5)线程同步相关接口

初始化条件变量

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

        参数:cond要初始化的条件变量   attr:暂不关注,置空即可

        返回值:成功返回0,失败返回错误码

销毁条件变量

int pthread_cond_desroy(pthread_cond_t *restrict cond);//销毁条件变量

等待条件满足

int  pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

       参数:cond等待的条件变量      mutex:互斥量

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

#include                                                                                                                                                                        
#include
#include
  
pthread_mutex_t mutex;
pthread_cond_t cond;
  
void* fun1(void* arg)
{
   while(1)
   {
     sleep(1);//每隔一秒发送一个信号
     pthread_cond_signal(&cond);
   }
}
  
void* fun2(void* arg)
{
    while(1)
    {
      //等待,当接收到信号就会执行下面输出语句。
      pthread_cond_wait(&cond,&mutex);
      printf("开始行动\n");
    }
}
  
int main()
{
    //初始化互斥量和条件变量
    pthread_mutex_init(&mutex,NULL);
    pthread_cond_init(&cond,NULL);
    //创建两个线程,一个发送信号,另一个接收到信号打印“活动”
    pthread_t t1,t2;
    pthread_create(&t1,NULL,fun1,NULL);
    pthread_create(&t2,NULL,fun2,NULL);
  
    //线程等待
    pthread_join(t1,NULL);
    pthread_join(t2,NULL);
  
    //销毁互斥量和条件变量
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}            

6)为什么pthread_cond_wait需要互斥量

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足时一直等下去条件都不会满足。所以必须要有一个线程通过某些操作,改变共享变量,使得原先不满足的条件变得满足并去通知等待的线程。
  • 条件变量的满足必然会牵扯到共享数据的变化,所以必须有互斥锁的保护,没有互斥锁就无法保证线程安全的获取修改条件变量。
  • 当一个线程发现条件不满足时,就要调用wait将自己挂起等待,挂起等待时是带锁等待的!!!如果不解锁,其他线程无法访问条件变量,条件永远也不会成立,该线程将一直等待下去。因此,这里的互斥量起到解锁的作用。

Linux---线程的同步和互斥_第9张图片

你可能感兴趣的:(Linux,多线程,linux,操作系统)