线程安全、同步与互斥机制以及死锁的产生与实现

线程安全:多个执行流对临界资源争抢访问,但是不会出现数据二义性。

线程安全的实现:

             同步:通过条件判断保证对临界资源访问的合理性。

             互斥:通过同一时间对临界资源访问的唯一性实现临界资源访问的安全性。

同步如何实现?/互斥如何实现?

             互斥的实现:互斥锁

互斥锁实现互斥的原理:互斥锁本身是一个只有0/1的计数器,描述了一个临界资源当前的访问状态,所有执行流在访问临界资源都需要判断当前的临界资源状态是否允许访问,如果不允许则让执行流等待,否则可以让执行流访问临界资源,但是在访问期间需要将状态修改为不可访问状态,这期间如果由其他执行流想要访问,则不被允许。

而信号量的互斥实现也是这个原理,只不过更多的是用于对资源进行计数实现同步。

互斥锁具体的操作流程及接口介绍

1.定义互斥锁变量 

pthread_mutex mutex;

2.初始化互斥锁变量

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

pthread_mutex mutex=PTHREAD_MUTEX_INITIALIZER;//定义并初始化的过程

3.在访问临界资源之前进行加锁操作(不能加锁则等待,可以加锁则修改资源状态,然后调用返回访问临界资源)

 pthread_mutex_lock(pthread_mutex_t *mutex);//这是一个阻塞操作,完了完成一个功能,发起调用,如果不具备完成功能的条件,则一直等待
 int pthread_mutex_trylock(pthread_mutex_t *mutex);//避免一直等待,非阻塞操作,不能加锁则报错返回EBUSY。

挂起等待:将线程状态置为可中断休眠状态--表示当前休眠;  被唤醒:将线程状态置为运行状态。
 4.在临界资源访问完毕之后进行解锁操作(将资源状态置为可访问,将其他执行流唤醒)
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁操作
5.销毁互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);//销毁互斥锁

代码如下:以抢黄牛票为例:

 结果如下:

 所有的执行流都需要通过同一个互斥锁实现互斥,意味着互斥锁本身就是一个临界资源,所有人都会访问。

互斥锁本身操作都不安全如何保证别人安全?

互斥锁本身操作首先必须得是安全的--互斥锁自身计数的操作是原子操作

不管mutex的状态是什么,一步交换之后其他的线程都是不可访问的,这时当前线程就可以慢慢判断。

寄存器的值与内存的值进行一步交换的过程:

         1.先将寄存器的值置为0

         2.直接将寄存器的值与内存空间中的数据进行交换--这个交换指令是一步完成的。(这时mutex值就为0,别人访问肯定无法加锁)

         3.判断寄存器中的值是否为1

            if(%eax==1)如果是1,则pthread_mutex_lock直接返回,访问临界资源%eax代表寄存器

            else 如果是0,则让线程等待。

死锁(!!!):多个执行流对锁资源进行争抢访问,但是因为互相申请被其他进程所占用不会释放资源处于一种永久等待状态。(是一种程序流程无法继续推进,卡在某个位置的一种概念)

死锁如何产生?

死锁的产生通常是在访问多个锁的时候需要注意的事项。

死锁的4个必要条件:

            1.互斥条件:一个资源每次只能被一个执行流使用(我加锁,别人不能加)
            2.不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺(我加的锁,别人不能解)

            3.请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

                                         (我加了A锁,去请求B锁,若不能对B锁加锁,则不释放A锁)
            4.循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

                                         (我加了A锁,去请求B锁,另一个人加了B锁,然后去请求A锁)

死锁的预防破坏死锁产生的必要条件(主要避免3个4条件的产生)

死锁的避免:死锁检测算法/银行家算法

银行家算法的思路:系统的安全状态/非安全状态

       一张表记录当前有哪些锁,一张表记录已经给谁分配了哪些锁,一张表记录谁当前需要哪些锁 按照3张表进行判断,判断若给一个执行流分配了指定的锁,是否会达成循环等待条件导致系统的运行进入不安全状态,如果有可能就分配,

反之,若分配之后不会造成循环等待条件梅西同时安全的,则分配这个锁。--破环循环等待条件

后续若不能分配锁,可以资源回溯,把当前执行流中已经加的锁释放掉--破环请求与保持

非阻塞加锁操作,若不能加锁,则把受伤其他锁也释放掉--破环请求与保持

高性能程序中通常会讲究一种无锁编程--CAS锁(乐观锁)和Synchronized锁(悲观锁)/一对一的阻塞队列/atomic原子操作

悲观锁:总是认为我在操作的时候别人会跟我抢,总是进行加锁。

乐观锁:总是乐观的认为我操作的时候别人没有操作。

同步并不保证安全/互斥并不保证合理

同步的实现:通过条件判断实现临界资源访问的合理性--条件变量

当前是否满足获取资源的条件,若不满足,则让执行流等待,等到满足条件能够获取的时候在唤醒执行流。

条件变量实现同步非常简单:只提供了两个功能接口:让执行流等待的接口和唤醒执行流的接口

因此条件的判断是需要进程自身进行操作,自身判断是否满足条件,不满足的时候调用条件变量接口使线程等待。

同步的接口如下

1.定义条件变量--pthread_cond_t  cond;

2.初始化条件变量--

pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *attr)

pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

3.使线程挂起休眠的接口:若资源获取条件不满足时调用接口进行阻塞等待

pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);--条件变量是搭配互斥锁一起使用的(判断条件是否满足的条件本身就是一个临界资源,需要被保护)--一直死等别人的唤醒

int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);--等待指定时间内都没有被唤醒则自动醒来

4.唤醒线程的接口:pthread_cond_signal(pthread_cond_t *)--唤醒至少一个等待的线程(并不是单个)

pthread_cond_broadcast(pthread_cond_t *)--唤醒所有等待的线程
5.销毁条件变量:pthread_cond_destroy(pthread_cond_t *);

我们举个例子理解一下通过条件变量与互斥锁实现同步的操作流程:

 多个顾客线程,因为没有饭而等待

一个厨师做好饭,调用pthread_cond_signal唤醒顾客线程,然而pthread_cond_signal唤醒的是至少一个线程,假设3个顾客线程都被唤醒,其中只有一个线程加锁成功,去吃饭,其他两个顾客线程卡在加锁这里,加锁成功的顾客线程吃饭,唤醒厨师,进行解锁(时间片调度到谁,谁运行)

这时加锁成功的不一定是厨师,有可能是卡在被唤醒后加锁的顾客线程--则这个线程在没有饭的情况下吃了一碗--逻辑错误。具体代码如下:

显示结果为:

如何避免上述逻辑错误条件的判断应该是一个循环判断顾客线程被唤醒加锁成功后重新判断有没有饭,没有就休眠,有则吃饭。

将上述代码中的临界变量bowl的判断if换成while,一对一的时候用if,多个线程对应多个线程的时候就应该用循环判断,但是程序会卡住。

结果如下:

条件变量只有一个,意味着等待队列只有一个,顾客没饭吃就要挂到等待队列上,厨师不能做饭也要挂到等待队列上,三个厨师线程,在有饭的情况下,就会挂到队列上。

假设一个顾客线程吃完饭,要唤醒厨师,唤醒一个厨师后,因为没有饭,重新挂到队列上

被唤醒的厨师,做了一顿饭之后要唤醒等待队列上的顾客,但是因为这时队列上既有厨师还有顾客,有可能厨师没有唤醒顾客,反而唤醒了一个厨师,被唤醒的厨师发现有饭,重新陷入休眠,导致程序阻塞。

解决程序卡死的方案:不同角色的线程应该应该挂在不同的等待队列上进行等待,唤醒的时候,就比较有目的性的唤醒,因此多个角色需要使用多个条件变量(因为一个条件变量只有一个队列)

 

结果如下:实现了多线程--逻辑正确

 

总结注意事项:

            1.条件变量使用中对条件的判断应该使用while循环

            2.多种角色线程应该使用多个条件变量

生产者消费者模型---对典型的应用场景设计的解决方案

生产者与消费者模型应用场景:有线程不断的生产数据线程不断的处理数据

数据的生产与数据的处理:放在同一个线程中完成,因为执行流只有一个,那么肯定是生产一个处理一个,处理一个后才能生产一个,这样的话依赖关系太强---如果处理比较慢,也会拖的生产速度慢下来。

因此将生产与处理放到不同的执行流中完成,中间增加一个数据缓冲区,作为中间的数据缓冲场所。

生产者与消费者解决的问题:1.解耦和 (将各个功能分离)2.支持忙闲不均/3.支持并发(cpu的轮询调度,多个执行流进行处理)

并发:轮询处理            并行:同时处理

 生产者与消费者模型的实现:其实只是两种业务处理的线程,创建线程就可以。实现的关键在于线程安全的队列。

封装一个线程安全的BlockQueue-阻塞队列--向外提供线程安全的入队/出队操作

class BlockQueue{

public:

BlockQueue();

//编码风格:纯输入参数-const &/输出型参数 指针/输入输入型 &

bool Push(const int &data);//入队数据

bool Pop(int *data);//出队数据

private:

std::queue<int> _queue;//STL中queue容器,是非线程安全--因为STL设计之初是奔着性能--并且功能多了耦合度就高

int  _capacity;//队列中节点的最大数量--数据也不能无限制添加,内存耗尽程序就崩溃了

pthread_mutex_t mutex;

pthread_cond_t productor_cond;//生产者队列

pthread_cond_t customer_cond;//消费者队列

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值