Linux下的线程安全

  • 线程安全:多个执行流对临界资源进行争抢访问,而不会造成数据二义性和逻辑混乱,成这段代码的过程是线程安全的。
  • 实现:保证多个执行流对临界资源进行争抢访问不造成数据二义性。
  • 同步与互斥
    1. 同步:通过条件判断,实现对灵界资源访问的时序合理性。
    2. 互斥:通过唯一访问,实现对临界资源的安全性。
一、互斥实现的技术:互斥锁/信号量
  • 原理:保证同一时间只有一个执行流对临界资源进行访问。
    即:对临界资源进行标记,无访问时标记为1,有访问时标记为0,当标记为1时,则用户可访问或线程可访问;当标记为0时,线程不可访问。
    先进行判断,能访问则访问,不能访问则休眠。
互斥锁:
  • mutex:是一个计数器。
  1. 互斥锁的代码操作流程:
  •   ①定义互斥锁变量: pthread_metux_t  mutex;
      ②初始化互斥锁:
      int pthread_mutex_init(pthread_mutex_t* mutex,pthread_mutexattr_t* attr);
      mutex :互斥索变量首地址;
      attr : 互斥锁属性,通常置为NULL。
      ③对临界资源访问时先加锁:
      两种方式:
      	阻塞加锁:int pthread_mutex_lock(pthread_mutex_t* mutex);
      	非阻塞加锁:int pthread_mutex_trylock(pthread_mutex_t* mutex);
      ④访问结束,记得解锁:
      int pthread_mutex_unlock(pthread_mutex_t* mutex);
      ⑤不用锁了,释放资源,销毁互斥锁:
      int pthread_mutex_destory(pthread_mutex_t* mutex);
    

代码如下:

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

int g_tickets = 100;//假设有100张火车票

//1. 定义互斥锁变量,并且这个变量也是一个临界资源,需要能够被各个线程访问到
//可以将互斥锁变量定义成为一个全局变量,也可以将其定义成局部变量,然后通过函数传参传递给线程
pthread_mutex_t mutex;

void *thr_Scalpers(void *arg)
{
    //黄牛的毕生工作--抢票
    //尽量避免对不需要加锁的操作进行加锁,会影响效率,因为在加锁期间别人都不能操作
    //若加锁的操作越多,意味着需要的时间就更长
    while(1) {
        pthread_mutex_lock(&mutex);//加锁一定是在临界资源访问之前,保护的也仅仅是临界区
        if (g_tickets > 0) {//当g_tickets=1的时候,判断成功,进入抢票流程
            usleep(1000);//但是抢票有个过程,在这个期间,其他黄牛,也有可能判断成功
            printf("I:[%p] got a train ticket:%d\n", pthread_self(), g_tickets);
            g_tickets--;
            //解锁是在临界资源访问完毕之后
            pthread_mutex_unlock(&mutex);
        }else {
            //在任意有可能退出u线程的地方,记得解锁
            //否则若退出没有解锁,则其它线程获取不到锁,就会卡死
            pthread_mutex_unlock(&mutex);
            pthread_exit(NULL);//没有票了就退出
        }
    }
    return NULL;
}
int main()
{
    int i = 0;
    pthread_t tid[4];
    //2. 初始化互斥锁,一定要在创建线程之前!!!
    pthread_mutex_init(&mutex, NULL);
    for (i = 0; i < 4; i++) {
        int ret = pthread_create(&tid[i], NULL, thr_Scalpers, NULL);
        if (ret != 0) {
            printf("thread create failed!!\n");
            return -1;
        }
    }

    for (i = 0; i < 4; i++) {
        pthread_join(tid[i], NULL); //等待普通线程退出,不想让主线程先退出
    }
    //5. 如果不使用互斥锁了,最终销毁互斥锁,释放资源
    pthread_mutex_destroy(&mutex);
    return 0;
}

注意事项:
(1) 加锁保护区域最好只是对临界资源进行加锁,因为保护的越多,执行所需要的时间越长,降低效率。
(2)加锁之后在任意有可能退出线程的地方都要进行解锁操作,若没有解锁直接退出,会造成其他线程访问锁时的状态。
2. 死锁:多个执行流对临界资源争抢访问时,因推进顺序不当,多条执行流相互等待,最终程序无法继续执行。

  • 必要条件:必须具备的条件,如果不具备就无法实现死锁。
    ①互斥条件:一个资源只能被一个执行流进行访问。
    ②不可剥夺条件:谁加的锁谁才能进行解锁操作,一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
    ③请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
    ④环路等待条件:线程1抢到锁A,去抢锁B,线程2抢到锁B去抢锁A。形成环路,进而产生死锁。
  • 避免死锁:
    ①破坏死锁的四个必要条件
    ②加锁顺序一致
    ③避免锁未释放的场景
    ④资源一次性分配
二、同步实现技术:条件变量/信号量
条件变量:
  • 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  • 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
  • 实现同步的思路是向用户提供两个接口(一个是让进程陷入阻塞等待的接口,一个是唤醒休眠的接口)+pcb等待队列。
  1. 条件变量的接口实现:
  •   ①定义条件变量:pthread_cond_t cond;
    
  •   ②初始化条件变量:int pthread_cond_init(pthread_cond_t* cond,pthread_condattr_t* attr);
    
  •   ③在不满足资源访问时:int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex);
      //cond :条件变量   mutex: 互斥量
    
  •   ④线程促使资源访问条件满足之后:
      int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒全部
      int pthread_cond_signal(pthread_cond_t *cond);//唤醒一个
    
  •   ⑤销毁:int pthread_cond_destory(pthread_cond_t* cond);
    
  1. 操作流程:
    (1)加锁;
    (2)用户自己进行条件判断,不能访问则调用pthread_cond_wait()陷入等待。
    (3)别唤醒之后,能够访问,则访问数据,获取资源。
    (4)唤醒生产资源的线程。
    (5)加锁。
    代码操作:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

int bowl = 0; // 初始为0,表示没有面
pthread_mutex_t mutex;
pthread_cond_t consumer_cond;
pthread_cond_t cook_cond;

void *thr_customer(void *arg)
{
    //这是一个顾客的流程
    while(1) {
        //0. 加锁操作
        pthread_mutex_lock(&mutex);
        while (bowl == 0) {//顾客加锁成功,发现没有饭,则要陷入休眠,如果没解锁就会导致厨师无法做饭
            //pthread_mutex_unlock();
            //----------------//厨师在这期间做好饭,唤醒就白唤醒了,因为顾客还没睡
            //pause();//休眠等待唤醒
            //pthread_mutex_lock();
            //如果没饭,则要等待,因为已经加过锁了,因此等待之前要解锁,被唤醒之后要加锁
            //因此pthread_cond_wait集合了三步操作:解锁/挂起/加锁
            //解锁和挂起是一个原子操作-不可被打断
            //顾客解锁,还没来得及挂起休眠;这时候厨师进来做饭,做好后唤醒顾客(实际顾客还没休眠)
            //会导致顾客这时候拿到时间片休眠彻底卡死(厨师不会进行第二次唤醒)
            pthread_cond_wait(&consumer_cond, &mutex);
        }
        //能走下来应该表示有饭了,bowl是等于1的
        printf("It's delicious.\n");
        bowl = 0; // 饭被吃完了
        //唤醒厨师,再做一碗
        pthread_cond_signal(&cook_cond);
        //解锁操作
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
void *thr_cook(void *arg)
{
    //这是一个厨师的流程
    while(1) {
        //0. 加锁操作,因为咬对碗进行操作
        pthread_mutex_lock(&mutex);
        while (bowl == 1) {
            //表示有饭,则不能做,陷入等待
            pthread_cond_wait(&cook_cond, &mutex);
        }
        //能走下来表示没饭,则做饭  bowl=0
        printf("Made a bowl of delicious rice\n");
        bowl = 1;//做了一碗饭
        //唤醒顾客
        pthread_cond_signal(&consumer_cond);
        //解锁操作
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main()
{
    pthread_t ctid[2]; 
    int ret, i;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&consumer_cond, NULL);
    pthread_cond_init(&cook_cond, NULL);

    for (i = 0; i < 4; i++) {
        ret = pthread_create(&ctid[0], NULL, thr_customer, NULL);
        if (ret != 0) {
            printf("create thread failed!!\n");
            return -1;
        }
    }
    for (i = 0; i < 4; i++) {
        ret = pthread_create(&ctid[1], NULL, thr_cook, NULL);
        if (ret != 0) {
            printf("create thread failed!!\n");
            return -1;
        }
    }

    pthread_join(ctid[0], NULL);
    pthread_join(ctid[1], NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&consumer_cond);
    pthread_cond_destroy(&cook_cond);
    return 0;
}

注意事项:
(1) pthread_cond_wait() 实现了三步操作:先解锁,陷入休眠,被唤醒后再加锁。
(2)用户自己进行的判断需要用到while循环判断。
(3)不同的角色应该等待在不同的条件变量上。

三、信号量:

强调不能与信号概念混淆

  • 作用:实现线程间的同步与互斥。
  • 本质:一个计数器+pcb等待序列。
  • 同步与互斥:
    (1)实现同步:计数器对资源数量进行计数,当线程想要获取资源的时候,先访问信号量,判断是否能够获取资源(通过计数器本身),若计数器<=0,则直接堵塞线程,计数器-1;当其他线程生产资源的时候,计数器+1,唤醒等待队列上的pcb。
    (2)实现互斥:保证计数器不大于1,就表示每次最多只有一个线程能够访问资源。
  1. 操作接口
  •  1.定义信号量sem_ t sem;
     2.初始化信号量int sem_ init(sem_ t *sem, int pshared, int value)
     3.在访问临界资源之前,先判断计数,是否能够访问资源,若不能访问,则阻塞线程;若可以访问则调用直接返回
     int sem_ wait(sem_ _t *sem) / int sem_ trywait(sem_ _t *sem)/ int sem_ timedwait(sem_ t *sem, struct timespec *ts)
     4.访问临界资源之后/生产资源之后,唤醒一个等待的线程,并且计数+ 1
     int sem_ post(sem_ t *sem);
     5.不使用信号量记得释放资源
     int sem_ destroy(sem. _t *sem);
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值