Linux-线程同步与互斥

线程互斥

线程互斥的产生条件

在了解线程的基本知识后,我们知道,对于线程而言,所有线程共享进程的地址空间。在进程中的文件描述符,信号处理方式,定义的全局变量,所有线程都可以看到。线程通过共享这些资源的方式,完成线程间的交互。

我们把这些可以共享的资源叫做临界资源。程序访问临界资源的代码区,叫做临界区。
那么当多个线程并发的去操作临界资源时,就会引发一些问题。比如访问一个全局变量。就会导致数据不一致问题。

为了证明我们所说的问题,来看下面代码(模拟买票系统)

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

int ticket=100;

void* route(  void *arg)
{
      char *id=(  char*)arg;
      while( 1)
      {

            if(ticket>0  )
            {
                  sleep(1);
                  printf(  "%s sells ticket %d\n",id,ticket);
                  ticket--;

            }
            else
            {

                  break;
            }
      }
}

int main(  )
{

    //创建4个线程,模拟四个用户买票
      pthread_t t1,t2,t3,t4;
      pthread_create(&t1,NULL,route,"thread_1");
      pthread_create(&t2,NULL,route,"thread_2");
      pthread_create(&t3,NULL,route,"thread_3");
      pthread_create(&t4,NULL,route,"thread_4");

      pthread_join(t1,NULL);
      pthread_join(t2,NULL);
      pthread_join(t3,NULL);
      pthread_join(t4,NULL);
      pthread_mutex_destroy(&mutex);
      return 0;

}

这里写图片描述

在这个例子中,票数ticket就是一个临界资源,当几个进程同时访问他时,结果就出现了图中标注的结果。下面来分析原因

  • ticket是非原子操作
    对ticket减 1操作,对应汇编代码,是三条语句
1.load      将共享变量从内存加载道寄存器
2 updata    更新寄存器里的值,进行减1操作
3 store     将新值从寄存器加载到内存中

在这三步操作中,由于不是原子操作,就会导致并没有执行完这三步操作,又会重新开始执行的问题,从而引发数据不一致问题。比如,当一个线程1,开始操作ticket变量(假设ticket的初始值为100),执行到第二步,将ticket减1,这时,由于线程切换,线程2开始运行,也访问ticket。此时,线程1刚刚对ticket减1,将ticket变成99,还没有来得及放回内存,线程2就进入这个临界区了。所以线程1不得不把99先保存起来。让出CPU给线程2。

线程2争取到CPU资源时,也对ticke减一操作。对于线程2,以及后面的线程,他们访问ticket时,都执行了完整的操作,即三步全部执行完,然后让出资源。知道ticket变成1,

当其他线程访问完ticket时,这时线程1被切回来了,他刚刚执行到第二步,他眼中的ticket是99。他将继续执行第三步,将99放回内存中去。

可是,现在内存中的ticket是1,(线程2,3.,4执行的结果)。对于同一个变量,既是1,又是99,这是不可能的。所以就出现了前面所说的数据不一致问题。

  • if条件为真,代码可以并发的切换到其他线程
  • sleep()这个模拟业务的漫长过程中,可以有很多线程进入临界区

要解决这三个 问题,需要做到以下三点:

  • 代码必须要有互斥行为:当代码进入临界区行时,不允许其他线程进入该临界区。

  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

互斥锁实现原理:

设置一个共享(锁)变量,其初始值为0.当一个线程进入临界区时,他首先测试这把锁。如果该锁的值是0,则该线程就将锁的值设置成1,并进入临界区。若这把锁的值是1,。则该进程将进行等待直到锁的值变为0。于是0就表示了临界区没有线程,1表示了某个线程进入了临界区。

这里写图片描述

互斥锁

初始化互斥量有两种方法:

方法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

销毁互斥量

销毁互斥量需要注意:

  • 使用PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread mutex t *mutex); int pthread_mutex_unlock(pthread mutex t *mutex);

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

调⽤用pthread_ lock 时,可能会遇到以下情况:

互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread lock调用会陷阻塞,等待互斥量解锁。

基于上面背景知识,我们现在对刚才的售票系统进行改进。给他加上互斥锁(只是处理此问题的方法之一)


#1include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

int ticket=100;
//定义一把锁
pthread_mutex_t mutex;
void* route(  void *arg)
{
      char *id=(  char*)arg;
      while( 1)
      {
            //加锁
            pthread_mutex_lock(&mutex);

            if(ticket>0  )
            {
                  sleep(1);
                  printf(  "%s sells ticket %d\n",id,ticket);
                  ticket--;
                  //释放锁
                  pthread_mutex_unlock(&mutex);

            }
            else
            {
                  //释放锁
                  pthread_mutex_unlock(&mutex);
                  break;
            }
      }
}

int main(  )
{

      //初始化锁
      pthread_mutex_init(&mutex,NULL);
      pthread_t t1,t2,t3,t4;
      pthread_create(&t1,NULL,route,"thread_1");
      pthread_create(&t2,NULL,route,"thread_2");
      pthread_create(&t3,NULL,route,"thread_3");
      pthread_create(&t4,NULL,route,"thread_4");

      pthread_join(t1,NULL);
      pthread_join(t2,NULL);
      pthread_join(t3,NULL);
      pthread_join(t4,NULL);
      //销毁锁
      pthread_mutex_destroy(&mutex);
      return 0;

}

这里写图片描述

现在就解决了多线程访问临界资源出现的数据不一致问题。根据代码,可以用一张图简明说明互斥机制(在上面代码中,我们对临界资源实现了加锁机制)

这里写图片描述

在图中,线程1在T1时刻申请到锁进入临界区。稍后,T2时刻,线程2尝试申请锁进入临界区。但是失败了,因为此时另一个线程已经在临界区,并没有解锁。所以线程2没有锁,但是他又要访问临界区资源,所以此时他就被暂时挂起,直到T3时刻线程1释放锁,离开临界区为止。线程2申请到锁,进入临界区。最后在T4时刻,B离开临界区。临界区就暂时处于没有线程进入的临时状态。

线程同步机制

在前面的例子中,我们解决了共享变量不一致的问题。
下面来进一步分析该问题。

我们知道,当为防止多个线程同时进入临界区引发数据不一致问题,可以给临界资源加上一把锁。线程申请到锁才有可能访问到临界资源。而其他没有申请锁的线程则会一直被挂起知道锁被释放。

我们知道,进程有优先级,线程也有优先级。当某个线程优先级很高,只要这个优先级很高的线程一到,其他线程都必须排到后面挂起等待。而这个高贵的线程就像一个纨绔的富家子弟一样。申请到锁,进入临界区,却然后又释放锁,如此回环往复。导致其他线程一直得不到临界资源。从而引发饥饿问题,无法完成任务。

比如在一个数据生产者线程和消费数据线程,消费者线程一直占用着互斥锁,数据消费完了依然霸占着锁。这样就会导致数据生产者无法生产数据,而消费者在消费完有限的数据后,也无法再正常消费。

为了解决这个问题,我们需要引入同步机制。实现线程与线程之间协同访问临界资源。就比如让生产者与消费者都可以访问到临界资源。正常工作。

线程同步方法
  • 条件变量
  • POSIX信号量

本文先介绍用条件变量实现线程同步

先来认识条件变量的相关接口
  • 初始化

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread condattr t *rest rict attr);

参数:

cond:要初始化的条件变量attr:NULL

  • 销毁
int pthread_cond_destroy(pthread_cond_t *cond)
  • 等待条件满足
int pthread_cond_wait(pthread cond t *restrict cond,pthread mutex_t *restrict mute x);

参数:cond:要在这个条件变量上等待mutex:
互斥量,后面详细解释

等待的时候必须释放锁,否则会造成死锁问题。
必须在指定的mutex互斥锁下面等待,且必须等待。

所谓等待,其实就是把pcb连接到条件变量的函数体内,并将进程状态从R状态变成非R状态。

  • 唤醒等待
唤醒所有线程
int pthread_cond_broadcast(pthread cond_t *cond);

唤醒指定线程
int pthread_cond_signal(pthread cond_t *cond);

下面来看简单运用

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>

//定义一个互斥锁和条件变量
pthread_mutex_t mutex;
pthread_cond_t cond;

//线程1的执行任务
void* r1(  void *arg)
{
      while(1)
      {
          //等待被唤醒后执行任务
            pthread_cond_wait(&cond,&mutex);
            printf("hello\n");
      }
}
//线程2的执行任务
void* r2(void *arg)
{
      while( 1)
      {
          //每隔1s唤醒线程1,提示可以执行任务了
            pthread_cond_signal(&cond);
            sleep(1);
      }
}

int main(  )
{

      //初始化互斥锁
      pthread_mutex_init(&mutex,NULL);
      //初始化条件变量
      pthread_cond_init(&cond,NULL);
      pthread_t t1,t2;
      //创建线程1,线程2
      pthread_create(&t1,NULL,r1,"thread_1");
      pthread_create(&t2,NULL,r2,"thread_2");
      //等待线程
      pthread_join(t1,NULL);
      pthread_join(t2,NULL);
      //销毁互斥锁和条件变量
      pthread_mutex_destroy(&mutex);
      pthread_cond_destroy(&cond);
      return 0;

}

这里写图片描述

从结果可以看出,线程1,只有在接收到线程2的通知,才开始执行任务。打印hello.

以上就是线程的互斥与同步机制简单介绍。在接下来的博客里,来实现基于同步互斥的生产者消费者模型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值