目录
🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴🛴
1.线程安全概念
1.1什么是线程不安全
在代码运行过程中,多个线程访问共享资源时,一个线程访问的共享资源被其他线程修改,导致线程拿到了错误的数据。这就出现了线程安全问题。
临界区:一次仅允许一个进程使用的共享资源。
临界区资源:每个进程中访问临界资源的那段程序称之为临界区。
进程进入临界区的调度原则
如果有若干进程请求进入空闲的临界区,一次仅允许一个进程进入,其它线程处于就绪或者等待状态
任何时候,处于临界区内的进程不可多于一个,若已有进程进入自己的临界区,则其它想进入自己临界区的进程必须等待。
进行临界区的进程要在有限时间内退出,以便其它进程能及时进入自己的临界区。
1.2.既然出现了线程不安全,那么如何解决线程不安全问题呢?
利用互斥和同步机制
互斥:控制线程的访问时序,当多个线程能够同时访问访问临界资源时,有可能会导致线程执行的结果产生二义性。而互斥就是要保证多个线程在访问同一个临界资源,执行临界区代码的时候,控制访问时序。即让一个线程独占临界资源执行完,再让另一个线程独占执行。
同步:为了保证多线程对临界资源访问的合理性。即互斥保证线程读写数据不会出错,而同步在互斥的基础上进行优化,增加了访问的合理性,提高了程序运行效率。同步的前提是互斥。
2.互斥的实现
2.1互斥锁
每个线程在访问临界资源之前需要拿到互斥锁,互斥锁一次只能被一个线程拿到
互斥锁原理
互斥锁本身就是0/1计数器,计数器的值只能取0或者取1。1表示当前线程可以获取到互斥锁,从而取访问临界资源. 0表示当前线程不可以获取到互斥锁,从而不能访问互斥资源。这里需要注意,加锁时,需要给所有可能出现不安全的线程加上同一个互斥锁。
2.2互斥锁原子性的保证
互斥锁的计数器当中的值和计数器内存的值交换,而交换是一条汇编指令就可以完成的,所以保证了原子性。
加锁的时候,将寄存器的值初始化为0。将寄存器的值和计数器中的值交换,交换后,若寄存器值为1,则加锁成功。若寄存器值为0,说明原来计数器值也是0,即加锁失败。
解锁的时候,寄存器中的值设置为1。将寄存器和计数器中的值进行交换,计数器的值一定是1,所以保证解锁成功。
2.3互斥锁接口
2.3.1初始化互斥锁
动态初始化互斥锁接口
int pthread_mutex_init( pthread_mutex_t* mutex,const pthread_mutexattr_t* attr); /* 参数: mutex:传递的互斥锁对象 attr: 互斥锁属性,一般传递NULL 返回值:初始化成功,返回0,失败则设置errno,并返回 */
静态初始化互斥锁的接口
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER; #define PTHREAD_MUTEX_INITIALIZER{{ ...}} //结构体初始化
2.3.2互斥锁加锁接口
阻塞加锁接口
int pthread_mutex_lock(pthread_mutex_t *mutex); /* 参数: mutex:传递互斥锁对象 返回值: 加锁成功,返回0,加锁失败就会阻塞等待,直到拿到锁 */
非阻塞加锁接口
int pthread_mutex_trylock(pthread_mutex_ *mutex); /* 参数:传递的互斥锁对象 返回值:加锁成功,返回0 注意:需要搭配循环,判断返回值使用 */
带超时时间加锁接口
int pthread_mutex_timelock(pthread_mutex_t *restrictmutex,const strut timespec *restrict abs_timeout); /* 参数:restrictmutex:锁对象 restrict abs_timeout:结构体,包含时间单位,秒级和纳秒级 注意: 调用带有超时时间的加锁接口 空闲的:直接加锁成功返回 忙碌的:等待时间范围内,阻塞等待,超出时间,锁若还未释放,该函数直接返回 */
2.3.3解锁接口
int pthread_mutex_unlock(pthread_mutex_t mutex_t *mutex); /* 参数:mutex:要解锁的对象 返回值:解锁成功返回0 解锁失败,设置erron并返回 */
2.3.4 销毁互斥锁接口
int pthread_mutex_destroy(pthread_mutex_t *mutex); /* 参数: mutex:传递要销毁的互斥锁对象 注意:如果是动态初始化互斥锁的,需要调用销毁接口。如果是静态初始化互斥锁,就不需要销毁 */
2.4代码验证锁的接口
#include<stdio.h> #include<pthread.h> #include<unistd.h> int glob=1000; pthread_mutex_t g_lock; void* my_thread_start(void* arg){ while(1){ pthread_mutex_lock(&g_lock); if(glob<0){ pthread_mutex_unlock(&g_lock); break; } printf("i am %p,glob val is %d\n",pthread_self(),glob); glob--; pthread_mutex_unlock(&g_lock); } return NULL; } int main(){ pthread_mutex_init(&g_lock,NULL); pthread_t tid[2]; int i; for( i=0;i<2;i++){ int ret=pthread_create(&tid[i],NULL,my_thread_start,NULL); if(ret<0){ perror("pthread_create"); return 0; } } //线程等待 int j; for(j=0;j<2;j++){ pthread_join(tid[j],NULL); } pthread_mutex_destroy(&g_lock); return 0; }
3.同步的实现
3.1条件变量
上边的互斥已经能够实现基本需求,但是我们现在想象这样一个场景。
桌子上有一个包,A负责往包里放糖果,B负责从包里拿糖果,现在需求是A放一个糖果,B拿一个糖果,这个时候我们实现就是在线程入口函数中加一个if判断,如果包里没有糖果,只能A拿到锁使用。若B拿到锁,就让它退出。这样在业务实现上确实没有问题。
那么假设这种场景,包里是空的,B拿到了锁,不满足条件,解锁,B退出。因为是抢占式执行,B又抢到了锁,B又进来,不满足条件,解锁,B退出。这样很浪费CPU资源。
所以条件变量应运而生,在判断时,条件变量可以将不满足条件的线程强制放入PCB等待队列,也就是将拿糖果的让他强制等待,放了糖果后再唤醒。
3.2条件变量的接口
3.2.1初始化接口(动态初始化)
int pthread_cond_init(pthread cond_t *restrict cond,const pthread_condattr_t *restrict attr);
/*
pthread_cond_t:条件变量类型
参数: cond
接受一个条件变量的指针或者地址
*/
3.2.2等待接口
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
/*
参数 cond:条件变量
mutex:互斥锁
谁调用就将谁放进条件变量的PCB等待队列中
*/
3.2.3唤醒接口
int pthread_cond_broadcast(pthread_cond_t * cond);
//唤醒PCB等待队列当中的所有线程
int pthread_cond_signal(pthread_cond_t * cond);
//唤醒PCB等待队列当中的至少一个线程
/*
参数:cond:条件变量
*/
3.2.4销毁接口
int pthread_cond_destroy(pthread_cond_t* cond);
//将条件变量销毁
3.3条件变量的代码演示
需求:多个线程往包里放糖,多个线程从包里拿糖,包里一次只能有一个糖。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#define pthread_cout 2
int glob_suga=0;
//定义互斥锁和条件变量
pthread_mutex_t g_lock;
pthread_cond_t pushsug;
pthread_cond_t popsug;
void* push_start_fun(void* arg){
while(1){
pthread_mutex_lock(&g_lock);
while(glob_suga==1){
printf("i am %p,包里有糖果,我去等待了!\n",pthread_self());
pthread_cond_wait(&pushsug,&g_lock);
}
printf("i am %p,包里没有糖果,我可以放%d个。\n",pthread_self(),++glob_suga);
pthread_mutex_unlock(&g_lock);
//通知拿糖的人
pthread_cond_signal(&popsug);
}
}
void* pop_start_fun(void* arg){
while(1){
pthread_mutex_lock(&g_lock);
while(glob_suga==0){
printf("i am %p,包里没有糖果,我去等待了\n",pthread_self());
pthread_cond_wait(&popsug,&g_lock);
}
printf("i am %p,包里有%d个糖果,我拿了\n",pthread_self(),glob_suga);
glob_suga--;
pthread_mutex_unlock(&g_lock);
//通知放糖的人
pthread_cond_signal(&pushsug);
}
}
int main(){
//初始化互斥锁和条件变量
pthread_mutex_init(&g_lock,NULL);
pthread_cond_init(&pushsug,NULL);
pthread_cond_init(&popsug,NULL);
pthread_t push[pthread_cout];
pthread_t pop[pthread_cout];
//创建两个线程
int i;
for(i=0;i<pthread_cout;i++){
int ret= pthread_create(&push[i],NULL,push_start_fun,NULL);
if(ret<0){
perror("pthread_create");
}
int ret2= pthread_create(&pop[i],NULL,pop_start_fun,NULL);
if(ret2<0){
perror("pthread_create");
}
}
//等待两个线程
for(i=0;i<pthread_cout;i++){
pthread_join(push[i],NULL);
pthread_join(pop[i],NULL);
}
//销毁互斥锁
pthread_mutex_destroy(&g_lock);
pthread_cond_destroy(&pushsug);
pthread_cond_destroy(&popsug);
return 0;
}
3.4条件变量的相关问题
条件变量等待接口第二个参数传递互斥锁是偶然吗?
因为访问到条件变量,就意味着它已经加锁,若不满足条件,条件变量就将线程放入PCB等待队列,此时 pthread_cond_wait其实是先放进等待队列,然后解锁,否则这个线程带着锁到等待队列,下一个进程 就没法进来了,造成了死锁,程序无法继续执行下去。
线程被唤醒后会执行什么代码?
执行唤醒操作时,会在线程内部进行加锁操作。在抢锁时,抢到了,pthread_cond_wait函数就执行完毕了,函数返回。若没抢到,pthread_cond_wait继续处于抢锁阶段。
4.生产者消费者模型代码实现
需求:实现一个生产者消费者模型,包含两个生产线程,两个消费线程和一个线程安全的队列,生产线程往队列中放数据,消费线程从队列中读取数据,进行处理。
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<queue>
using namespace std;
class SafeQueue{
private:
//队列
queue<int> _que;
int _capcity;
//互斥锁
pthread_mutex_t g_lock;
//条件变量
pthread_cond_t pro;
pthread_cond_t con;
public:
SafeQueue(){
pthread_mutex_init(&g_lock,NULL);
pthread_cond_init(&con,NULL);
pthread_cond_init(&pro,NULL);
_capcity=10;
}
~SafeQueue(){
pthread_mutex_destroy(&g_lock);
pthread_cond_destroy(&con);
pthread_cond_destroy(&pro);
}
void push(int date){
pthread_mutex_lock(&g_lock);
while(_que.size()>=10){
pthread_cond_wait(&pro,&g_lock);
}
_que.push(date);
printf("i am product thread:%p,i product %d\n",pthread_self(),date);
pthread_mutex_unlock(&g_lock);
pthread_cond_signal(&con);
}
void pop(int *date){
pthread_mutex_lock(&g_lock);
while(_que.size()<=0){
//把消费者放到等待队列
pthread_cond_wait(&con,&g_lock);
}
*date=_que.front();
_que.pop();
printf("i am consume thread:%p,i consume %d\n",pthread_self(),*date);
pthread_mutex_unlock(&g_lock);
//通知生产者
pthread_cond_signal(&pro);
}
};
#define p_count 2
//消费者线程
void* my_con(void* arg){
SafeQueue* sq= (SafeQueue*)arg;
while(1){
int date;
sq->pop(&date);
}
}
//生产者线程
pthread_mutex_t g_data_lock = PTHREAD_MUTEX_INITIALIZER;
void* my_pro(void* arg){
SafeQueue* sq= (SafeQueue*)arg;
int data=0;
while(1){
pthread_mutex_lock(&g_data_lock);
sq->push(data);
data++;
pthread_mutex_unlock(&g_data_lock);
}
}
int main(){
SafeQueue *sq = new SafeQueue();
//创建线程
pthread_t _con[p_count],_pro[p_count];
int i;
for(i=0;i<p_count;i++){
int ret= pthread_create(&_pro[i],NULL,my_pro,(void*)sq);
if(ret<0){
perror("pthread_create");
}
ret= pthread_create(&_con[i],NULL,my_con,(void*)sq);
if(ret<0){
perror("pthread_create");
}
}
//让主线程等待
for(i=0;i<p_count;i++){
pthread_join(_pro[i],NULL);
pthread_join(_con[i],NULL);
}
delete sq;
return 0;
}