Linux多线程(中)

Linux多线程(中)

1.线程安全

什么是线程不安全?

多个线程并发执行一段代码时,会导致程序结果的二义性
假设有两个线程A,B,有一个CPU,两个线程想同时对全局变量i=10进行加加,如果A从内存中读到i的值后还没有加加,线程就切换了,此时切换成线程B从内存中读到了i并加加为11,B加完后轮到线程A,线程A之前读到的值为10加完后i的值仍然是11;另一种情况则是A加完后B再加,此时的到值就是12。因此产生了结果二义性的问题。

下面是一个不安全线程的代码:

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

int g_ticket = 100000;

void* my_thread_start(void* arg){
    //修改全局变量
    while(g_ticket > 0){
        printf("i am %p, cout g_i val is %d\n", pthread_self(), g_ticket);
        g_ticket--;
    }
    return NULL;
}

int main(){
    //1.创建线程
    //  两个工作线程修改全局变量
    pthread_t tid[2];
    int i;
    for(i = 0; i < 2; i++){
        int ret = pthread_create(&tid[i], NULL, my_thread_start, NULL);
        if(ret < 0){
            perror("pthread_create");
            return 0;
        }
    }


    //2. 主线程 (只要不退出就好)
    for( i = 0; i < 2; i++){
        pthread_join(tid[i], NULL);
    }
    return 0;
}

在这里插入图片描述

那么什么是线程安全?

  • 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

那么该如何解决线程不安全的问题呢?可以通过下面的三点来解决。

代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

而要做到这三点,本质上就是需要一把锁,即互斥锁

2.同步与互斥

进程线程间的互斥相关背景概念:

  • 临界资源:多线程执行流共享的资源就叫做临界资源,如上代码中的:
    在这里插入图片描述
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区,如上代码中的:
    在这里插入图片描述
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
  • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
1.互斥锁

原理:

互斥锁的本质是0/1计数器(互斥量),计数器的取值只能为0/1
计数器的值为1:表示当前线程可以获取到互斥锁,从而去访问临界资源
计数器的值为0:表示当前线程不可以获取到互斥锁,从而不能访问临界资源
注意:并不是说线程不获取互斥锁就不能访问临界资源,而是程序员需要在代码中使用一个互斥锁,去约束多个进程。

为什么计数器中的值01变化是原子性的呢?

直接使用寄存器当中的值和计数器内存的值交换,而交换是一条汇编指令就可以完成的(底层的操作一步完成)
加锁的时候:寄存器当中的值设置为(0)
第一种情况:计数器的值为1,说明锁空闲,没有被线程加锁,就可以加锁成功
在这里插入图片描述
第二种情况:计数器的值为0,说明锁忙碌,被其他线程加锁拿走了
在这里插入图片描述
解锁的时候,寄存器当中的值设置为(1),计数器的值为0,需要解锁,进行一步交换
在这里插入图片描述

接口:

动态初始化:int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr);
静态初始化:pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIXER;
#define PTHREAD_MUTEX_INITIALIXER {{0,0,0,0,0…}}
加锁:int pthread_mutex_lock(pthread_mutex_t *mutex);

代码实现:

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

int g_ticket = 100;
pthread_mutex_t g_lock;

void* my_thread_start(void* arg){
    //修改全局变量
    pthread_mutex_lock(&g_lock);
    while(g_ticket > 0){
        printf("i am %p, cout g_i val is %d\n", pthread_self(), g_ticket);
        g_ticket--;
    }
    return NULL;
}

int main(){
    //0.初始化互斥锁
    pthread_mutex_init(&g_lock, NULL);
    //1.创建线程
    //  两个工作线程修改全局变量
    pthread_t tid[2];
    for(int i = 0; i < 2; i++){
        int ret = pthread_create(&tid[i], NULL, my_thread_start, NULL);
        if(ret < 0){
            perror("pthread_create");
            return 0;
        }
    }


    //2. 主线程 (只要不退出就好)
    for(int i = 0; i < 2; i++){
        pthread_join(tid[i], NULL);
    }
    return 0;
}

解锁接口:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex:传递互斥锁变量
注意:在线程所有可能退出的地方都进行解锁!否则可能导致死锁(退出的线程将互斥锁拿走了,其他等待线程永远不可能拿到互斥锁了)

销毁接口:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
如果是动态初始化互斥锁的,需要调用销毁接口。如果是静态初始化互斥锁的,就不需要销毁了

代码实现:

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

int g_ticket = 1000;
pthread_mutex_t g_lock;

void* my_thread_start(void* arg){
    //修改全局变量
    while(1){
        pthread_mutex_lock(&g_lock);
        if(g_ticket <= 0){
            pthread_mutex_unlock(&g_lock);
            break;
        }
        printf("i am %p, cout g_i val is %d\n", pthread_self(), g_ticket);
        g_ticket--;
        pthread_mutex_unlock(&g_lock);
    }
    return NULL;
}

int main(){
    //0.初始化互斥锁
    pthread_mutex_init(&g_lock, NULL);
    //1.创建线程
    //  两个工作线程修改全局变量
    pthread_t tid[2];
    for(int i = 0; i < 2; i++){
        int ret = pthread_create(&tid[i], NULL, my_thread_start, NULL);
        if(ret < 0){
            perror("pthread_create");
            return 0;
        }
    }


    //2. 主线程 (只要不退出就好)
    for(int i = 0; i < 2; i++){
        pthread_join(tid[i], NULL);
    }

    //3.释放互斥锁
    pthread_mutex_destroy(&g_lock);
    return 0;
}

2.同步

有了互斥之后为什么还要有同步?

多个线程保证了互斥,也就是保证了线程能够独占访问临界资源,但并不是说,各个线程在访问临界资源的时候都是合理的
而同步出现了,为了保证多个线程对临界资源访问的合理性,这个合理性建立在保证互斥的情况下

什么是条件变量?

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

  • 使用原理:线程在加锁后,判断下临界资源是否可用
    如果可用,则直接访问临界资源;如果不可用,则调用等待接口,让该线程进行等待
  • 原理:本质上是PCB等待队列(存放在等待的线程的PCB)

条件变量的接口:

  • 初始化接口:int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
    pthread_con_t:条件变量类型
    参数:cond,接受一个条件变量的指针,接受一个条件变量的地址
    attr:表示条件变量的属性信息,传递NULL,采用默认属性
    静态初始化:pthread_cond_t cond=PTREAD_COND_INITALIZER;
  • 等待接口:int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
    参数:cond,条件变量
    mutex:互斥锁
  • 唤醒接口:int pthread_cond_broadcast(pthread_cond_t *cong);//唤醒PCB等待队列当中的所有线程
    int pthread_cond_signal(pthread_cond_t *cond)j,//唤醒PCB等待都一列当中至少一个线程
  • 销毁接口:int pthread_cond_destroy(pthread_cond_t *cond);条件变量的销毁

代码实现(建立做面与吃面的线程):

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

#define THREAD_COUNT 2

//代表碗里的情况:0表示没面,1表示有面
int g_bowl=0;

pthread_mutex_t g_lock;
pthread_cond_t g_eat_cond;
pthread_cond_t g_make_cond;

//工作线程
void* eat_thread_start(void* arg){
  while(1){
    pthread_mutex_lock(&g_lock);
    //没面等待
    while(g_bowl==0){
      pthread_cond_wait(&g_eat_cond,&g_lock);
    }
    
    printf("我是 %p,碗里有面,我可以吃:%d\n",pthread_self(),g_bowl--);
    pthread_mutex_unlock(&g_lock);

    //通知做面的人做面
    pthread_cond_signal(&g_make_cond);
  }
}

//工作线程
void* make_thread_start(void* arg){
  while(1){
    pthread_mutex_lock(&g_lock);
    //有面等待
    while(g_bowl==1){
      pthread_cond_wait(&g_make_cond,&g_lock);
    }
    printf("我是%p,碗里没有面,我可以做面了,%d\n",pthread_self(),g_bowl++);
    pthread_mutex_unlock(&g_lock);

    pthread_cond_signal(&g_eat_cond);
  }
}

int main(){
  //1。初始化互斥锁
  pthread_mutex_init(&g_lock,NULL);
  pthread_cond_init(&g_eat_cond,NULL);
  pthread_cond_init(&g_make_cond,NULL);

  //2.创建吃面的线程和做面的线程
  pthread_t eat[THREAD_COUNT],make[THREAD_COUNT];
  int i;
  for(i=0;i<THREAD_COUNT;i++){
    int ret=pthread_create(&eat[i],NULL,eat_thread_start,NULL);
    if(ret<0){
      perror("pthread_create");
      return 0;
    }

    ret=pthread_create(&make[i],NULL,make_thread_start,NULL);
    if(ret<0){
      perror("pthread_create");
      return 0;
    }
  }

  //3.等待两种线程
  for(i=0;i<THREAD_COUNT;i++){
    pthread_join(eat[i],NULL);
    pthread_join(make[i],NULL);
  }

  //4.销毁互斥锁
  pthread_mutex_destroy(&g_lock);
  pthread_cond_destroy(&g_eat_cond);
  pthread_cond_destroy(&g_make_cond);

  return 0;
}

在这里插入图片描述

深入了解:

  • 条件变量的的等待接口第二个参数为什么会有互斥锁?
    等待接口谁调用,就将谁放到条件变量对应的PCB等待队列中;
    一是为了在线程访问临界资源之前,一定是加锁访问的,保证互斥属性
    二是传递给pthread_cond_wait接口,就是想让进行解锁
  • pthread_cond_wait的内部是针对互斥锁做了什么操作?先释放互斥锁还是将线程放入到PCB等待队列
    先放到pcb等待队列在进行解锁
  • 线程被唤醒之后会执行什么代码,需要在获取互斥锁吗?
    被唤醒之后,线程再次判断有无临界资源可以访问,有就访问,没有就继续等待
4.死锁

产生场景:

1.线程加锁之后,并没有释放互斥锁
2.两种线程分别拿着一把锁,还想请求对方的锁

代码举例:

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

#define THREAD_COUNT 1

//代表碗: 0表示没有面, 1表示有面
int g_bowl = 0;

pthread_mutex_t g_lock1;
pthread_mutex_t g_lock2;


void* eat_thread_start(void* arg){
    pthread_mutex_lock(&g_lock2);
    sleep(1);
    pthread_mutex_lock(&g_lock1);

    pthread_mutex_unlock(&g_lock1);
    pthread_mutex_unlock(&g_lock2);
}

void* make_thread_start(void* arg){
    pthread_mutex_lock(&g_lock1);
    sleep(1);
    pthread_mutex_lock(&g_lock2);

    pthread_mutex_unlock(&g_lock2);
    pthread_mutex_unlock(&g_lock1);
}

int main(){
    //1.初始化互斥锁
    pthread_mutex_init(&g_lock1, NULL);
    pthread_mutex_init(&g_lock2, NULL);


    //2.创建吃面的线程 和 做面的线程
    pthread_t eat[THREAD_COUNT], make[THREAD_COUNT];
    for(int i = 0; i < THREAD_COUNT; i++){
        int ret = pthread_create(&eat[i], NULL, eat_thread_start, NULL);
        if(ret < 0){
            perror("pthread_create");
            return 0;
        }

        ret = pthread_create(&make[i], NULL, make_thread_start, NULL);
        if(ret < 0){
            perror("pthread_create");
            return 0;
        }
    }


    //3.等待两种线程
    for(int i = 0; i < THREAD_COUNT; i++){
        pthread_join(eat[i], NULL);
        pthread_join(make[i], NULL);
    }

    //4.销毁互斥锁
    pthread_mutex_destroy(&g_lock1);
    pthread_mutex_destroy(&g_lock2);
    return 0;
}

死锁的必要条件:

  • 不可剥夺:线程获取到互斥锁之后,除了自己释放,其他线程不能进行释放
  • 循环等待:线程A拿着1锁请求2锁,线程B拿着2锁去请求1锁
  • 互斥条件:一个互斥锁在同一时间只能被一个线程所有
  • 请求与保持:吃着碗里的,看着锅里的

代码如何避免死锁?

破坏必要条件:循环等待 请求与保持
加锁顺序一致,都先加1锁,再加2锁
避免锁没有被释放:在所有可能线程退出的地方都进行解锁
资源一次性分配:多个资源在代码中有可能每一个资源都需要使用不同的锁进行保护

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值