线程安全-----互斥锁+条件变量+信号量+线程安全的单例模式

线程安全

概念:多个线程(执行流)同时对临界资源进行访问而不会造成数据二义

实现:同步 + 互斥
同步:对临界资源访问的时序合理性
互斥:同一时间访问的唯一性

线程间互斥的实现:互斥锁

1.定义互斥锁变量

pthread_mutex_t mutex

2.对互斥锁变量进行初始化

pthread_mutex_init(&mutex,&attr)
参数:mutex:要初始化的互斥量
        attr:互斥量属性,常置NULL

3.对临界资源进行加锁保护

pthread_mutex_lock(&mutex)
//这是一个阻塞加锁,可以加锁就加锁,不可以就等待
pthread_mutex_trylock  //非阻塞加锁,尝试加锁,不成功直接报错返回
pthread_mutex_timedlock  /非阻塞加锁,一直尝试加锁,到了超时时间还没成功就报错返回

4.对临界资源操作完毕后解锁

pthread_mutex_unlock(&mutex);

注意:用户在加锁之后,需要在任意有可能退出线程的地方进行解锁

5.销毁互斥锁

pthread_mutex_destroy(&mutex);

销毁互斥锁时的注意事项:

1.使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁 
2.不要销毁一个已经加锁的互斥量 
3.已经销毁的互斥量,要确保后面不会有线程再尝试加锁

黄牛抢票的例子

#include 
#include 
#include 

pthread_mutex_t mutex;//定义互斥锁
int ticket = 100;
void *yellow_bull(void *arg)//入口函数
{
    while(1) {
        pthread_mutex_lock(&mutex);//加锁
        if (ticket > 0) {
            usleep(1000);
            printf("bull %d get a ticket:%d\n", (int)arg, ticket);//打印票号
            ticket--;
        }else {
            printf("have no tickets, bull %d exit\n", (int)arg);//没票时退出
            //用户在加锁之后,需要在任意有可能退出线程的地方进行解锁
			//没票了如果直接退出则下面无法解锁,其他线程一直等待,无法退出
            pthread_mutex_unlock(&mutex);
            pthread_exit(NULL);
        }
        pthread_mutex_unlock(&mutex);//解锁
    }
}
int main()
{
    pthread_t tid[4];//创建四个线程
    pthread_mutex_init(&mutex, NULL);//初始化互斥锁
    int i;
    for (i = 0; i < 4; i++) {
        int ret = pthread_create(&tid[i], NULL, yellow_bull, (void*)i);
        if (ret != 0) {
            printf("thread create error\n");
            return -1;
        }
    }
    for (i = 0; i < 4; i++) {
        pthread_join(tid[i], NULL);//线程循环等待
    }
    //int pthread_mutex_destroy(pthread_mutex_t *mutex);
    //mutex:    互斥锁变量
    pthread_mutex_destroy(&mutex);//销毁互斥锁
    return 0;
}

死锁
概念:多个线程对锁资源进行竞争访问,但是因为推进顺序不当,导致相互等待,使得程序无法继续往下运行

死锁产生的四个条件
1.互斥条件- - -一个锁只有一个线程可以获取
2.不可剥夺条件- - -我加的锁别人不能解
3.请求与保持条件- - -拿着A锁,去请求B锁,但是获取不到B锁,也不释放A锁
4.环路等待条件- - -我拿着A锁请求B锁,对方拿着B锁请求A锁

死锁预防:破坏四个必要条件

死锁避免:银行家算法
银行家算法:
银行家算法是一个用来避免系统进入死锁状态的算法,用它可以判断系统的安全性,如果系统当前处于安全状态,则可以为申请资源的进程分配资源,如果不是安全状态,则不能为申请资源的进程分配资源。
银行家算法执行过程中,首先判断申请资源的进程所申请的资源数目是否合法,若是合法的,则可以为其进行试分配,再利用安全性算法求出安全序列,·如果存在安全序列,则说明可以给申请资源的进程分配资源,分配成功,继续为其它进程服务。如果找不到安全序列,则说明为该进程分配资源后系统会进入不安全状态,所以不能为该进程分配资源,使该进程进入阻塞状态。若申请资源的进程申请的资源数目不合法,则不需要进行试分配,直接使其进入阻塞状态,处理其他申请资源的进程。
参考自这里

条件变量

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题

线程之间实现同步:等待 + 唤醒
操作不满足条件则等待,其他线程使条件满足之后唤醒

条件变量实现同步:
线程对临界资源访问之前,先判断是否能够操作,若可以操作则线程直接操作,若不能,则条件变量提供等待功能,让pcb等待在队列上,其他线程促使条件满足,然后唤醒条件变量等待队列上的线程

注意:
条件变量只提供等待和唤醒功能,具体何时等待,何时唤醒,需要用户自己判断

条件变量函数

1.定义条件变量

pthread_cond_t cond

2.条件变量初始化

pthread_cond_init(&cond,&attr);
参数cond:要初始化的条件变量
attr:通常置NULL

3.用户在判断条件不满足时提供等待功能

pthread_cond_wait(&cond,&mutex);
参数cond:要在这个条件变量上等待
mutex:互斥量

4.用户在促使条件满足后,唤醒等待

pthread_cond_signal(&cond)  唤醒所有等待的线程
pthread_cond_broadcast(&cond)  唤醒至少一个线程

5.销毁条件变量

pthread_cond_destroy(&cond)

为什么pthread_ cond_ wait 需要互斥量?
线程何时等待需要一个判断条件,这个判断条件也是临界资源,等待后其他线程需要促使这个条件满足(修改临界资源),所以一定要用互斥锁来保护。没 有互斥锁就无法安全的获取和修改共享数据。

条件变量的条件判断应该是一个循环判断:
拿厨师做面,顾客来吃面举例(厨师和顾客都是多个线程)
多个顾客线程若同时被唤醒,只有一个顾客可以加锁吃面,其他顾客线程将阻塞在加锁上,而不是条件变量的等待队列上,第一个加锁的顾客吃完面后解锁,这时获取到时间片加锁的可能是另一个顾客线程,顾客如果直接去吃面的话,发现面已经被上一个顾客吃掉了,所以存在逻辑错误,应该在吃面之前先判断是否有面

不同的对象应该放在不同的等待队列
如果一个厨师做了一碗面,同时唤醒所有的厨师和顾客线程,这时另一个厨师拿到时间片后加锁,判断有面,再次等待,进入循环判断,一直都有面,一直等待,但是在等待之前并没有唤醒顾客,导致程序卡死,没人吃面,也没人做面,所以不同的对象应该放在不同的等待队列,用两个条件变量

代码实现

*           以顾客吃面和厨师做面为例子:
*           顾客想要吃面,前提是有面,没有面则等待
*           厨师做面,做好面后,唤醒吃面的人
================================================================*/

#include 
#include 
#include 

int _have_noodle = 0;
pthread_mutex_t mutex;//定义一个互斥锁
pthread_cond_t cond_eat;//定义顾客条件变量
pthread_cond_t cond_cook;//定义厨师条件变量
void *eat_noodle(void *arg) 
{
    while(1) {//拿到锁之后要先判断有没有面
        pthread_mutex_lock(&mutex);
        while (_have_noodle == 0) {
            //休眠之前应该先解锁
            //pthread_cond_wait实现了三步操作:
            //  1. 解锁
            //  2. 休眠
            //  3. 被唤醒后加锁
            //  其中解锁和休眠操作必须是原子操作,不可被打断
            pthread_cond_wait(&cond_eat, &mutex);//等待在顾客队列
        }
        //能走下来表示have_noolde==1 ,表示有面
        printf("eat noodle\n");
        _have_noodle = 0;
        pthread_cond_signal(&cond_cook);//唤醒厨师
        pthread_mutex_unlock(&mutex);//解锁
    }
    return NULL;
}
void *cook_noodle(void *arg) 
{
    while(1) {//拿到锁之后要先判断有没有面
        pthread_mutex_lock(&mutex);
        while (_have_noodle == 1) {
            //现在有面,但是没人吃,不能继续做了,等待在厨师队列
            pthread_cond_wait(&cond_cook, &mutex);
        }
        printf("cook noodle\n");
        _have_noodle = 1;
        pthread_cond_signal(&cond_eat);//有面了,唤醒吃面的人
        pthread_mutex_unlock(&mutex);//解锁
    }
    return NULL;
}
int main()
{
    pthread_t tid1, tid2;

    pthread_mutex_init(&mutex, NULL);
    //int pthread_cond_init(pthread_cond_t *cond,
    //  const pthread_condattr_t *attr);
    pthread_cond_init(&cond_eat, NULL);
    pthread_cond_init(&cond_cook, NULL);
	//四个吃面和四个厨师
    for (int i = 0; i < 4; i++) {
        int ret = pthread_create(&tid1, NULL, eat_noodle, NULL);
        if (ret != 0) {
            printf("pthread create error\n");
            return -1;
        }
    }
    for (int i = 0; i < 4; i++) {
        int ret = pthread_create(&tid2, NULL, cook_noodle, NULL);
        if (ret != 0) {
            printf("pthread create error\n");
            return -1;
        }
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&mutex);
    //int pthread_cond_destroy(pthread_cond_t *cond);
    pthread_cond_destroy(&cond_cook);
    pthread_cond_destroy(&cond_eat);
    return 0;
}

信号量sem

遵循POSIX标准,支持平台移植

功能:实现线程之间的同步与互斥

本质:一个计数器(判断当前是否可以对临界资源进行操作)+等待队列+等待+唤醒

原理:

  1. 互斥原理:只具有0/1计数时,就可以实现互斥
    初始计数为1,表示当前只有一个线程能够获取资源,获取资源之后-1;计数为0,其他线程在等待队列上等待,临界资源被操作完毕之后计数+1,并且唤醒等待队列上的线程

  2. 同步原理:对程序逻辑进行控制(对临界资源合理操作控制)
    通过计数判断当前能否对临界资源进行操作,不能操作(计数<=0),则等待;其他线程操作后计数+1,促使再次判断,唤醒等待的线程

条件变量与信号量实现同步的区别
1.信号量并不需要搭配互斥锁的使用
2.信号量本身的计数就是是否能够对临界资源进行操作的判断条件;但是条件变量需要用户判断

sem信号量的使用流程:
1.定义信号量

sem_t

2.初始化信号量

int sem_init(sem_t *sem,int pshared,unsigned int value);
sem:信号量变量
pshared:0-表示用于线程间同步与互斥;非0表示进程间同步与互斥
value:信号量计数器初始值

3.判断计数是否可以对临界资源进行操作
若计数>0,函数立即正确返回,返回之前计数-1,其他线程可以继续操作临界资源;
若计数<=0,认为当前不能对临界资源进行操作,线程等待

int sem_wait(sem_t *sem);
sem:信号量变量

4.计数+1,唤醒等待队列上的线程

int sem_post(sem_t *sem);
sem:信号量变量

5.销毁信号量

int sem_destroy(sem_t *sem);
sem:信号量变量

sem信号量的简单操作

#include 
#include 
#include 
#include 
#include //注意sem的头文件

int ticket = 100;
sem_t sem;//定义信号量

void *thr_start(void *arg)//线程入口函数
{
    while(1) {
        //int sem_wait(sem_t *sem);
        //  若当前计数<=0 ;则线程一直等待
        sem_wait(&sem);//同加锁和解锁的位置一样
        if (ticket > 0) {
            printf("get a ticket:%d\n", ticket);
            ticket--;
        }else {
            sem_post(&sem);//解锁
            pthread_exit(NULL);
        }
        //int sem_post(sem_t *sem)
        //计数+1,促使其它线程操作条件满足,然后唤醒所有等待线程
        sem_post(&sem);
    }
    return NULL;
}
int main ()
{
    int i, ret;
    pthread_t tid[4];//创建四个线程
	
	//信号量初始化
    //int sem_init(sem_t *sem, int pshared, unsigned int value);
    sem_init(&sem, 0, 1);
    for (i = 0; i < 4; i++)  {
        ret = pthread_create(&tid[i], NULL, thr_start, NULL);
        if (ret != 0) {
            printf("pthread create error\n");
            return -1;
        }
    }

    for (i = 0; i < 4; i++)  {
        pthread_join(tid[i], NULL);
    }
    //int sem_destroy(sem_t *sem);
    sem_destroy(&sem);
    return 0;
}

单例模式

单例模式属于常见设计模式的一种
设计模式是大佬们针对经典的常见的场景设计出的对应的解决方案

单例模式的特点
一个对象只能被实例化一次(一个资源只能被加载一次)

单例模式的实现:

饿汉方式:资源在初始化时进行加载

特点:在程序初始化时资源加载时间较长,但之后运行流畅

class info{
public:
       static int _data;
	   int* getdata(){
	   return &_data;
	   }
};

int info::data_data=10;  //静态成员在类外初始化
int main(){
    info a;
	info b;
	a._data=20;
	
	std::out<<"a.data:"<<*a.getdata()<

输出全为20,因为a和b引用的是同一个对象

懒汉方式:资源在使用时才加载

特点:资源在使用时才被加载,在多个执行流中就可能被加载多次;因此资源的加载和对象的实例化过程需要受保护,线程安全的单例模式主要针对的就是懒汉方式的实现

class info{
public:
       static int *_data;
	   int* getdata(){
	   pthread_mutex_lock(&mutex);
	     if(_data==NULL){//判断是否被加载过
		    _data=new int;
			*_data=10;
		 }
	   pthread_mutex_unlock(&mutex);
	   return _data;
	   }
};

int* info::data_data=NULL;
int main(){
    info a;
	info b;
	
	std::out<<"a.data:"<<*a.getdata()<

你可能感兴趣的:(Linux操作系统,线程安全,锁,条件变量,信号量)