线性(下)----线程安全

线程安全

因为进程中线程共享了进程中的虚拟地址空间,所以线程间的通信更加方便,但数据有可能存在争抢关系,缺乏数据的访问控制,多个线程并发容易造成数据混乱,所以数据安全访问变得很重要。
造成数据混乱的的两个经典模型

同步与互斥概念

同步:线程/进程之间对临界资源的顺序访问关系(对临界资源访问的时序性)
互斥:线程/进程之间对临界资源的同一时间的唯一访问性关系

生产者与消费者模型

一个场所,两个角色,三种关系
生产者与生产者的关系:互斥(来保证数据的安全操作)
生产者与消费者的关系:同步和互斥
消费者与消费者的关系:互斥

如何来解决线程中数据的安全访问?------->实现线程间互斥

线程间的互斥实现:互斥锁(互斥量)
线程间的同步实现:条件变量
POSIX信号量:既可以实现同步可以实现互斥,既可以用于进程间的同步互斥,也可以实现线程间的同步互斥。

在互斥锁中死锁的必要条件?—如何避免
条件变量----等待和通知
为什么条件变量和互斥锁一起使用?
对于实现同步关键在于等待和通知,因为等待需要被唤醒,被唤醒的前提条件就是条件已经满足,并且这个条件本身就是一个临界资源。

互斥锁(或互斥量)-----实现线程间的互斥

互斥锁原理:
互斥锁以排他的方式防止共享数据被并发访问,是一个二元变量,
本质就是一个计数器,计数器只有0/1,在处理临界资源时要先申请互斥锁。互斥锁处于开锁状态,申请到互斥锁后立即占有该锁(加锁),防止其他线程访问资源。只有当前锁定该互斥锁的线程才可以释放该互斥锁。
在这里插入图片描述
互斥锁操作接口

  1.定义一个互斥锁//在线程创建之前完成
  定义一个互斥量(变量) pthread_mutex_t   name
  2.初始化互斥锁
			
				互斥锁的初始化有两种方式:
					
					1.定义时赋值初始化,不需要手动释放
							
						pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
						
					2.函数接口初始化,需要手动释放

						int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
							//参数一 : 互斥锁的变量
							//参数二 : 互斥锁的属性,一般设置为NULL

							返回0成功,返回非0就是错误								


3.对临界操作进程加锁或解锁
加锁:
int pthread_mutex_lock(pthread_mutex_t* mutex);//阻塞式申请,如果锁被锁住则等待锁被打开,即若该锁是锁顶状态,默认阻塞当前进程。	
int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞加锁,获取不到锁立即报错返回 	
int pthread_mutex_timedlock(pthread_mutex_t* restrict mutex, const struct timespec *restrict abs_timeout);	//限时阻塞加锁,如果获取不到锁则指定等待时间,这段时间完了还没获取到,则报错返回
	
解锁(释放):int pthread_mutex_unlock(pthread_mutex_t *mutex);//在任意一个有任何可能性退出的地方都要解锁						
			

						
																
  4.销毁互斥锁
				pthread_mutex_destroy(pthread_mutex_t *restrict mutex);

死锁情况:一直获取不到锁资源而造成的锁死情况
死锁产生的必要条件:必须具备以下条件才能满足
全部具备以下条件:
1.互斥条件----一个获取另外一个就不能获取
2.不可剥夺条件----一个线程获取锁只能由这个线程自己释放
3.请求与保持条件----获取第一个锁之后又去获取第二个锁
4.环路等待条件----a拿了锁1去申请锁2,而b拿了锁2去申请锁1,形成环路死锁
如何预防产生死锁:破坏死锁产生的必要条件
避免产生死锁:银行家算法(在这个算法中定义了两个状态,安全状态,非安全状态,如果某一步操作操作完毕后处于安全状态,那么可以执行,如果处于不安全状态那么就不能执行)

线性间互斥实例:


/*
这是一个买票的例子
每一个黄牛都是一个线程,在这个例子中有一个总票数ticket
 *  每一个黄牛买到一张票这个ticket都会-1,直到票数为0
 */

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

int ticket = 100;

//互斥锁的初始化有两种方式:
//  1. 定义时直接赋值初始化,最后不需要手动释放
//  2. 函数接口初始化,最后需要手动释放
//  pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

pthread_mutex_t mutex;   //定义互斥锁

void *y_cow(void *arg)
{
    int id = (int)arg;
    while(1) {
        //2. 加锁操作
        //  int pthread_mutex_lock(pthread_mutex_t *mutex);
        //      阻塞加锁,如果获取不到锁则阻塞等待锁被解开
        //  int pthread_mutex_trylock(pthread_mutex_t *mutex);
        //      非阻塞加锁,如果获取不到锁则立即报错返回EBUSY
        //  int pthread_mutex_timedlock (pthread_mutex_t *mutex,
        //          struct timespec *t);
        //      限时阻塞加锁,如果获取不到锁则等待指定时间,在这段
        //      时间内如果一直获取不到,则报错返回,否则加锁
        pthread_mutex_lock(&mutex);
        if (ticket > 0) {
            usleep(100); //如果没有进行加锁操作,当票等于1时在睡眠的这个时间,很多线程都会进入,就会导致买到附属的票
            printf("y_cow:%d get a ticket:%d!!\n", id, ticket);
            ticket--;
        }else {
            printf("have no ticket!!exit!!\n");
            //**加锁后,在任意有可能退出的地方都要进行解锁,
            //**否则会导致其他线程阻塞卡死
            pthread_mutex_unlock(&mutex);
            pthread_exit(NULL);
        }
        //int pthread_mutex_unlock(pthread_mutex_t *mutex);
        //  解锁
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main()
{
    pthread_t tid[4];
    int i = 0, ret;

    //1. int pthread_mutex_init(pthread_mutex_t *mutex, 
    //          const pthread_mutexattr_t *attr);
    //  互斥锁的初始化
    //      mutex: 互斥锁变量
    //      attr:互斥锁的属性,NULL;
    //  返回值:0-成功      errno-错误
    pthread_mutex_init(&mutex, NULL);
    for (i = 0; i < 4; i++) {
        ret = pthread_create(&tid[i], NULL, y_cow, (void*)i);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
    }
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    pthread_join(tid[2], NULL);
    pthread_join(tid[3], NULL);
    //4. 销毁互斥锁
    pthread_mutex_destroy(&mutex);
    return 0;
}


条件变量—实现线程间的同步

条件变量的原理:
互斥锁能够解决对资源的互斥访问,但有些情况互斥并不能解决

同步说的是对公共资源的时序访问,若有资源,则一个线程就会来访问,如果没有资源则线程就会等待,条件变量发生改变时就会进行通知,线程就会做相应工作。所以条件变量用于等待某个条件被触发。
在这里以生产消费者模型来详细说明一下同步:
在这里插入图片描述

条件变量不能单独使用,需要和互斥锁配合使用,因为线程等待被唤醒,被唤醒的前提是“条件改变了”,例如没有产品时 ,消费者等待,有产品时,消费者被唤醒,有无产品就是“这个条件”。
线程同步实现代码:

/*  这是一个实现生产者与消费者同步的代码,生产者消费者分别代表一个线程
 *  有一个篮子,这个篮子是判断条件,
 *  篮子里有面
 *      代表消费者可以获取面,通知生产者面已经取走了
 *      代表生产者需要等待
 *  篮子里没有面
 *      代表消费者等待
 *      代表生产者放面,通知消费者面已经放了
 */

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

//1. 定义条件变量
//  条件变量的初始化有两种方式
//      1. 定义赋值初始化,不需释放
//      2. 函数接口初始化, 需要释放
//      pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond;   //定义条件变量
pthread_mutex_t mutex;  //定义互斥锁
int basket = 0;         

//卖面的
void *sale_noddle(void *arg)
{
    while(1) {
        pthread_mutex_lock(&mutex);  //basket就是是一个判断条件,线程都能访问
		    //比如有多个生产者时(多个线程),对于这一个公共数据,那么就会有争抢行为,需要互斥锁
        if (basket == 0) {           //加锁实现了对这个全局变量(公共资源的保护)
            printf("sale noddle!!!\n");
            basket == 1;  //生产了面,然后开始通知对方,唤醒消费者,使其不再等待
            //int pthread_cond_broadcast(pthread_cond_t *cond);
            //  唤醒所有等待在条件变量上的线程
            //int pthread_cond_signal(pthread_cond_t *cond);
            //  唤醒第一个等待在条件变量上的线程
            pthread_cond_signal(&cond);
        }
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
void *buy_noddle(void *arg)
{
    while(1) {
        pthread_mutex_lock(&mutex);  //加锁保护
        if (basket == 0) {
            //没有面就要等待
            //int pthread_cond_wait(pthread_cond_t *cond,
            //          pthread_mutex_t *mutex);第二个参数就是互斥锁 
            //  pthread_cond_wait的功能就是用来阻塞等待某个条件变量。它做的事情就是先解锁然后进入等待
           
            //  pthread_cond_wait函数先对互斥锁做了一个判断是否加锁,如果加锁了就解锁
            //  然后陷入等待*******整个过程是原子操作,不可被打断。
            //
            //  要防止的情况就是:假如没有面,而消费者又速度比较
            //  快,先拿到锁了,那么生产者将拿不到锁,没法生产将会
            //  造成双方卡死
            //  所以如果消费者如果先获取到锁,那么在陷入等待之前需
            //  要解锁
            
            pthread_cond_wait(&cond, &mutex);
        }
        printf("buy noddles!!!\n");
        basket = 0;
        pthread_mutex_unlock(&mutex);  //解锁
    }
    return NULL;
}
int main()
{
    pthread_t tid1, tid2;
    int ret;
    //1. 条件变量的初始化
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
    ret = pthread_create(&tid1, NULL, sale_noddle, NULL);
    if (ret != 0) {
        printf("pthread_create error\n");
        return -1;
    }
    ret = pthread_create(&tid2, NULL, buy_noddle, NULL);
    if (ret != 0) {
        printf("pthread_create error\n");
        return -1;
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    //4. 条件变量的销毁
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
    return 0;
}

POSIX 标准信号量----既可以实现同步也可以实现互斥

即可用于进程也可以用于线程

POSIX信号量实现线程间的同步和互斥

**信号量本质:**具有一个等待队列的计数器

线程同步实现:
消费者:没有资源则等待
生产者:生产出来资源则通知等待队列中的等待者

/*  这是验证使用信号量还实现线程间同步与互斥的代码
  信号量的操作步骤:
 *          1. 信号量的初始化
 *          2. 信号量的操作(等待/通知)
 *          3. 信号量的释放
 *      1. 同步:等待与通知
 */

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

sem_t sem;
//线程间同步与互斥
void *thr_producer(void *arg)
{
    while(1) {
        //生产者
        sleep(1);
        printf("make a hot beef noddle!!\n");
        //生产出资源后要通知等待在信号量上的线程/进程 
        //int sem_post(sem_t *sem);
        //信号量修改的是自己内部的资源计数,这个内部的资源计数就是
        //条件,而条件变量修改的是外部的条件,需要我们用户来修改
        sem_post(&sem);
    }
    return NULL;
}
void *thr_consumer(void *arg)
{
    while(1) {
        //消费者
        //2. 没有资源则等待
        //阻塞等待,没有资源则一直等待有资源,否则获取资源
        //int sem_wait(sem_t *sem);
        //非阻塞等待,没有资源则报错返回,否则获取资源
        //int sem_trywait(sem_t *sem);
        //限时等待,没有资源则等待指定时长,这段时间内有资源则获取
        //一直没有资源则超时后报错返回
        //int sem_timedwait(sem_t *sem,struct timespec *timeout);
        sem_wait(&sem);
        printf("very good!!!\n");
    }
    return NULL;
}
int ticket = 100;
void *buy_ticket(void *arg)
{
    while(1){
        //大家都是黄牛!!
        //因为计数器最大是1,也就代表只有一个线程能够获取到信号量
        //这样也就保证了同一时间只有一个线程能操作
        sem_wait(&sem);
        if (ticket > 0) {
            usleep(1000);
            ticket--;
            printf("cow %lu,buy a ticket:%d\n", ticket);
        }
        //操作完毕之后,对计数器进行+1,这时候信号量资源计数就又可
        //以获取了,然后又进入新一轮的资源争抢,因为资源计数只有一
        //个,因此也只有一个线程能够抢到
        sem_post(&sem);
    }
    return NULL;
}
int main()
{
    pthread_t tid1, tid2;
    int ret;

    //1. 初始化信号量
    //int sem_init(sem_t *sem, int pshared, unsigned int value);
    //  sem:信号量变量
    //  pshared:
    //          0-用于线程间
    //          非0-用于进程间
    //  value:信号量的初始计数
    ret = sem_init(&sem, 0, 1);
    if (ret < 0) {
        printf("init sem error!!\n");
        return -1;
    }
    /*
    //创建生产者线程
    ret = pthread_create(&tid1, NULL, thr_producer, NULL);
    if (ret != 0) {
        printf("pthread_create error\n");
        return -1;
    }
    //创建消费者线程
    ret = pthread_create(&tid2, NULL, thr_consumer, NULL);
    if (ret != 0) {
        printf("pthread_create error\n");
        return -1;
    }
    */
    //黄牛买票线程
    pthread_t tid;
    int i = 0;
    for (i = 0; i < 4; i++) {
        ret = pthread_create(&tid, NULL, buy_ticket, NULL);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
    }
    pthread_join(tid, NULL);
    //3. 销毁信号量
    //int sem_destroy(sem_t *sem);
    sem_destroy(&sem);
    return 0;
}


资源争抢的另外一种模型------读写者模型(理解即可),实现读写模型的安全数据访问是用—读写锁

读写者模型:
大量读,少量写。
写的时候他人不能读,
读的时候不能写,
写的时候他人不能写,
读的时候他人可以读

互相关系 :
读写之间互斥
写于写之间互斥
读和读没有关系
读写锁的实现------读写锁了解即可

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值