多线程
线程安全
线程安全:多个执行流对临界资源争抢访问,但是不会出现数据二义性
线程安全的实现
同步:通过条件判断保证对临界资源访问的合理性
互斥:通过同一时间对临界资源访问的唯一性实现临界资源访问的安全性
同步是如何实现/互斥又是如何实现?
互斥的实现:互斥锁
互斥锁实现互斥原理:互斥锁本身是一个只有0/1的计数器,描述了一个临界资源当前的访问状态,所有执行流在访问临界资源资源都需要判断当前的临界资源状态是否允许访问,如果不允许则让执行流等待,否则可以让执行流访问临界资源,但是在访问期间需要将状态修改为不可访问状态,这期间如果由其它执行流想要访问,则不被允许。
互斥
互斥锁具体的操作流程以及接口介绍:
1.定义互斥锁变量
pthread_mutex_t mutex;
2.初始化互斥锁变量
pthread_mutex_init(pthread_mutex* mutex, pthread_mutexattr_t* arr)
使用接口初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
使用宏初始化
3.在访问临界资源之前进行加锁操作(不能加锁则等待,可以加锁则修改资源状态,然后调用返回,访问临界资源)
pthread_mutex_lock(pthread_mutex* mutex)
阻塞加锁----如果当前不能加锁(锁已经被别人加了),则一直等待直到加锁成功调用返回
pthread_mutex_trylock(pthread_mutex* mutex)
非阻塞加锁----能加锁就加锁,不能加锁就报错返回
4.在临界资源访问完毕之后进行解锁操作(将资源状态置为可访问,将其他执行流唤醒)
pthread_mutex_unlock(pthread_mutex_t* mutex)
5.销毁互斥锁
pthread_mutex_destroy(pthread_mutex_t* mutex)
eg:
/*以黄牛党抢票为例*/
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
int ticket = 100;
pthread_mutex_t mutex;
void* thr_scalpers(void* arg){
while(1){
pthread_mutex_lock(&mutex);
if(ticket>0){
//有票就一直抢
usleep(1000);
printf("I got a ticket: %d\n", ticket);
ticket--;
pthread_mutex_unlock(&mutex);
}
else{
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
}
return NULL;
}
int main(){
pthread_t tid[4];
int i;
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 error");
}
}
for(i = 0; i < 4; i++){
pthread_join(tid[i], NULL);
}
pthread_mutex_destroy(&mutex);
return 0;
}
所有的执行流都需要通过同一个互斥锁实现,意味互斥锁本身就是一个临界资源,大家都访问。如果互斥锁本身的操作都不安全如何保证别人安全,所以互斥锁本身的操作首先必须是安全的;----互斥锁自身的操作是原子操作
不管当前mutex的状态是什么,反正一部交换之后,其他线程都是不可访问的;这时候当前线程就可以慢慢判断了
具体:
1.先将寄存器的值置为0
2.直接加你个寄存器的值与内存空间中的数据进行交换----这个交换指令是一步可以完成的(这时候内存中的mutex的值就是0了,别人访问肯定发现无法加锁)
3.if(mutex == 1){
则pthread_mutex_lock直接返回,访问临界资源
}
else{
则让线程等待
}
死锁
多个执行流对锁资源进行争抢访问,但是因为访问推进顺序不当,造成互相等到程序流程无法继续推进,这时候就造成了死锁。
死锁实际是一种程序流程无法继续推荐,卡在某个位置的一种概念,死锁的产生通常是在访问多个锁的时候需要注意的事项。
死锁产生的必要条件:(有一条不满足就不会产生死锁)
1.互斥条件:一个资源同一时间只有一个人能够访问,我加了锁,别人就不能继续加锁。
2.不可剥夺条件:我加的锁别人不能解,只有我能解锁
3.请求与保持条件:我加了A锁,然后去请求B锁;如果不能对 B锁加锁,则也不释放A锁。
4.环路等待条件:我加了A锁,然后去请求B锁;另一个人加了B锁,然后请求A锁
死锁预防:破坏死锁产生的必要条件(主要避免3和4两个条件的产生)
死锁的避免:死锁检测算法/银行家算法
银行家算法思路: 系统的安全状态/非安全状态
一张表记录当前有哪些锁,一张表记录已经给谁分配了哪些锁,一张表记录谁当前需要哪些锁。按照三张表进行判断,判断若给一个执行流分配了指定的锁,是否会达成环路等待条件导致系统的运行进入不安全状态,如果有可能就不能分配,反之,若分配了之后不会造成环路等待,系统是安全的,则分配这个锁。若不能分配锁,可以资源回溯,把当前执行流已经加的锁释放掉。
同步
同步的实现
线程在满足资源的访问条件的时候才能去访问资源,否则就挂起线程;直到满足条件之后再唤醒线程;----条件变量
条件变量:向外提供了一个使线程等待的接口和唤醒线程的接口 + pcb等待队列
因为条件变量只提供了使线程等待和唤醒的接口,因此什么时候让线程等待/唤醒就需要程序员在进程中进行判断(访问条件是否满足的判断由我们自己完成)
操作流程
1.定义条件变量
pthread_cond_t
2.初始化条件变量
pthread_cond_init(pthread_cond_t*,pthread_condattr_t*)
PTHREAD_COND_INITIALIZER
3.使线程挂起休眠的接口:条件变量需要搭配互斥锁一起使用
pthread_cond_wait(pthread_cond_t*, pthread_mutex_t*)
----一直等待别人的唤醒
pthread_cond_timewait(pthread_cond_t*, pthread_mutex_t*, struct timespec)
----等待指定时间内都没有被唤醒则自动醒来
4.唤醒线程的接口
pthread_cond_signal(pthread_cond_t*)
----唤醒至少一个等待线程
pthread_cond_broadcast(pthread_cond_t*)
----唤醒所有等待线程
5.销毁条件变量
pthread_cond_destroy(pthread_cond_t*)
例:单消费者 和 生产者 模型
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
int bowl = 0; //默认0表示碗中没有饭
pthread_cond_t cond; //实现线程间对bowl变量访问的同步操作
pthread_mutex_t mutex; //保护bowl变量的访问操作
void* thr_cook(void* arg){
while(1){
pthread_mutex_lock(&mutex);
if(bowl != 0){ //表示有饭,不满足做饭的条件
//让厨师线程等待,等待之前先解锁,被唤醒之后再加锁
//pthread_cond_wait接口中就完成了解锁,休眠,被唤醒后加锁三部分
//并且解锁和休眠操作时一步完成,保证原子操作
pthread_cond_wait(&cond, &mutex);
}
bowl = 1; //能够走下来表示没饭,bowl == 0,则做一碗饭,将bowl修改为1
printf("i made a bowl of rice\n");
//唤醒顾客吃饭
pthread_cond_signal(&cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* thr_customer(void* arg){
while(1){
pthread_mutex_lock(&mutex); //加锁
if(bowl != 1){ //没有饭,不满足吃饭条件,则等待
//没有饭则等待,等待前先解锁,被唤醒后加锁
pthread_cond_wait(&cond, &mutex);
}
bowl = 0; //能够走下来表示有饭,bowl == 1;吃完饭,将bowl修改为0
printf("i had a bowl of rice\n");
//唤醒厨师做饭
pthread_cond_signal(&cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main(){
pthread_t cook_tid, customer_tid;
int ret;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cond, NULL);
ret = pthread_create(&cook_tid, NULL, thr_cook, NULL);
if(ret != 0){
printf("pthread_create error\n");
return -1;
}
ret = pthread_create(&customer_tid, NULL, thr_customer, NULL);
if(ret != 0){
printf("pthread_create error\n");
return -1;
}
pthread_join(cook_tid, NULL);
pthread_join(customer_tid, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
多个顾客线程,因为没有饭而等待,一个厨师做好饭,调用pthread_cond_signal唤醒顾客线程,然而pthread_cond_signal唤醒至少一个线程,假设三个顾客线程都被唤醒其中只有一个顾客加锁成功去吃饭,其他两个顾客线程卡在加锁这里,加锁成功的顾客吃饭,唤醒厨师进行解锁(时间片调度到谁,谁运行),这时候加锁成功的不一定是厨师,有可能是卡在被唤醒后加锁的顾客线程----则这个线程没有饭的情况下吃了一碗----逻辑错误。这种逻辑错误的避免:条件的判断应该是一个循环判断,顾客线程被唤醒加锁成功后重新判断有没有饭,没有就休眠,有则吃饭。
条件变量只有一个,意味着等待队列只有一个,顾客没饭吃就要挂到等待队列上,厨师不能做饭也要挂在等待队列上,三个厨师在有饭的情况下,就会挂在等待队列上。假设一个顾客线程吃完饭,要唤醒厨师,唤醒一个厨师后,因为没有饭,重新挂到队列上。
解决方案:不同角色的线程应该挂在不同线程应该挂在不同的等待队列上进行等待,唤醒的时候,就比较有目的性的唤醒。
例: 多消费者 和生产者 模型
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
int bowl = 0; //默认0表示碗中没有饭
pthread_cond_t cook_cond; //实现线程间对bowl变量访问的同步操作
pthread_cond_t customer_cond;
pthread_mutex_t mutex; //保护bowl变量的访问操作
void* thr_cook(void* arg){
while(1){
pthread_mutex_lock(&mutex);
while(bowl != 0){ //表示有饭,不满足做饭的条件
//让厨师线程等待,等待之前先解锁,被唤醒之后再加锁
//pthread_cond_wait接口中就完成了解锁,休眠,被唤醒后加锁三部分
//并且解锁和休眠操作时一步完成,保证原子操作
pthread_cond_wait(&cook_cond, &mutex);
}
bowl = 1; //能够走下来表示没饭,bowl == 0,则做一碗饭,将bowl修改为1
printf("i made a bowl of rice\n");
//唤醒顾客吃饭
pthread_cond_signal(&customer_cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void* thr_customer(void* arg){
while(1){
pthread_mutex_lock(&mutex); //加锁
while(bowl != 1){ //没有饭,不满足吃饭条件,则等待
//没有饭则等待,等待前先解锁,被唤醒后加锁
pthread_cond_wait(&customer_cond, &mutex);
}
bowl = 0; //能够走下来表示有饭,bowl == 1;吃完饭,将bowl修改为0
printf("i had a bowl of rice\n");
//唤醒厨师做饭
pthread_cond_signal(&cook_cond);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
int main(){
pthread_t cook_tid[4], customer_tid[4];
int ret;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&cook_cond, NULL);
pthread_cond_init(&customer_cond, NULL);
int i = 0;
for(i = 0; i < 4; i++){
ret = pthread_create(&cook_tid[i], NULL, thr_cook, NULL);
if(ret != 0){
printf("pthread_create error\n");
return -1;
}
}
for(i = 0; i < 4; i++){
ret = pthread_create(&customer_tid[i], NULL, thr_customer, NULL);
if(ret != 0){
printf("pthread_create error\n");
return -1;
}
}
pthread_join(cook_tid[0], NULL);
pthread_join(customer_tid[0], NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cook_cond);
pthread_cond_destroy(&customer_cond);
return 0;
}
1.条件变量使用中对条件的判断应该使用while循环
2.多个角色线程应该使用多个条件变量
生产者消费者模型
大佬们针对典型的应用场景设计的解决方案
生产者与消费者模型应用场景:有线程不断生产数据,有线程不断处理数据
数据的生产与数据的处理,放在同一线程中完成,因为执行流只有,那么肯定是生产一个处理一个,处理完一个后生产一个。这样依赖关系太强----如果处理比较慢,也会拖的生产速度慢下来。因此将生产与处理放到不同执行流中完成,中间增加一个数据缓冲区,做为中间的数据缓冲场所。
生产者与消费者模型解决的问题:1.解耦合 / 2.支持忙闲不均 / 3.支持并发
并发:轮询处理
并行:同时处理
而这里的支持并发:指的是可以有多个执行流进行处理
生产者与消费者,其实是两种业务处理的线程而已----我们创建线程就可以
实现的关键在于线程安全的队列
#define QUEUE_MAX 5;
class BlockQueue{ //线程安全阻塞队列---没有数据则消费者阻塞/数据满了则生产者阻塞
public:
BlockQueue(int maxq = QUEUE_MAX):_capacity(maxq){
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&_productor_cond, NULL);
pthread_cond_init(&_customer_cond, NULL);
}
~BlockQueue(){
pthread_mutex_destroy(&mutex, NULL);
pthread_cond_destroy(&_productor_cond, NULL);
pthread_cond_destroy(&_customer_cond, NULL);
}
bool push(const element& data){ //入队数据
//只有生产者才会入队数据,如果队列中数据满了则需要阻塞
pthread_mutex_lock(&mutex);
while(_queue.size() == _capacity){
pthread_cond_wait(&productor_cond, &mutex);
}
_queue.push(data); //queue.push()入队操作
_pthread_mutex_unlock(&mutex);
_pthread_cond_signal(&_customer_cond);
return true;
}
bool pop(element* data){ //出队数据
//只有消费者才会出队数据,如果队列中数据没有则需要阻塞
pthread_mutex_lock(&mutex);
while(_queue.size() == 0){
pthread_cond_wait(&customer_cond, &mutex);
}
*data = _queue.front();
_queue.pop(); //queue.push()入队操作
_pthread_mutex_unlock(&mutex);
_pthread_cond_signal(&_productor_cond);
return true;
}
}
private:
queue<int> _queue;
int _capacity; //队列中节点最大数量
pthread_mutex_t _mutex;
pthread_cond_t _productor_cond; //生产者队列
pthread_cond_t _customer_cond; //消费者队列
};
void* thr_productor(void* arg){
BlockQueue* queue = (BlockQueue*) arg;
int i = 0;
while(1){
//生产者不断生产数据
queue->push(i);
printf("productor push data: %d\n", i++);
}
return NULL;
}
void* thr_customer(void* arg){
BlockQueue* queue = (BlockQueue*) arg;
while(1){
//消费者不断消耗数据
int data;
queue->pop(&data);
printf("customer pop data: %d\n", data);
}
return NULL;
}
int main(){
int ret, i;
pthread_t ptid[4], ctid[4];
BlockQueue queue;
for(i = 0; i < 4; i++){
ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&queue);
if(ret == 0){
printf("create productor pthread error");
return -1;
}
ret = pthread_create(&ctid[i], NULL, thr_customer, (void*)&queue);
if(ret == 0){
printf("create customer pthread error");
return -1;
}
}
for(i = 0; i < 4; i++){
pthread_join(ptid[i], NULL);
pthread_join(ctid[i], NULL);
}
return 0;
}