目录
注:如果本篇博客有任何错误和建议,欢迎伙伴们留言,你快说句话啊!
概念:
临界资源:指的是多个线程都能访问到的资源;
临界区:能够影响临界资源的代码区域
原子性操作:原子代表该操作仅仅有两个结果要么执行成功要么失败
线程安全:
首先我们要探讨什么样的程序是安全的呢?
最重要的一点,是在任何时候运行时,都能产生我们预期的结果,不会出现错误;
同样,线程安全,我们简单定义就是多个线程运行时,不会导致程序出现二义性结果。
我们可以参照下面的程序,加深对线程安全的理解:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
#define PTHREAD_NUM 2
int g_val=0;
void* my_thread(void* arg)
{
(void*)arg;
for(int i=0;i<100000000;++i)
{
g_val++;
}
return NULL;
}
int main()
{
for(int i=0;i<10;++i)
{
g_val=0;
pthread_t tid[PTHREAD_NUM];
for(int i=0;i<PTHREAD_NUM;++i)
{
int ret=pthread_create(&tid[i],NULL,my_thread,(void*)&g_val);
if(ret!=0)
{
return -1;
}
}
for(int i=0;i<PTHREAD_NUM;++i)
{
int ret=pthread_join(tid[i],NULL);
if(ret!=0)
{
return -1;
}
}
printf("%d\n",g_val);
}
return 0;
}
在上面的程序中,我们有一个临界资源 g_val,其值为0 ;之后我们启动两个线程进行++操作,每个线程执行100000000次++操作,两个线程执行完毕之后打印g_val,无论执行多少次,我们期待的结果都是200000000;然而,我们启动改程序,得到的结果并非如此,如图所示,出现了几个并非我们所预料的结果,这证明线程并不是安全的
线程不安全的原理:
刚才我们看到了线程不安全的现象,现在我们来分析一下出现二义性结果的原因是什么?
为了方便我们分析,我们简化一下问题:
现在有两个线程A,B和一个全局变量g_val=0; 每个线程都在入口函数中执行对全局变量g_val执行一次++操作;分析可能导致的结果:
首先我们看一下汇编代码执行++操作需要进行哪些步骤:
1.先将其保存到寄存器当中
2. 将其++
3.保存到内存当中
我们讨论的问题比较简单:无非有两种结果,预期的结果2,和结果1;接下来我们就讨论一下结果1是怎么得来的;
从上面我们已经知道++操作并不是原子性的,完成这个操作需要进行三个动作,而在进行这三个动作的时候,有可能会被操作系统打断,eg:时间片耗尽,进行切换
- 倘若线程A刚从内存当中取到g_val的值即把0保存到寄存器当中,就切换出去了;线程拥有自己的程序计数器:保存下一个即将执行的指令;上下文信息:保存寄存器当中的值;当再次拥有CPU时,会凭此恢复。
- 这时候线程B取到了g_val的值即把0保存到了寄存器,CPU进行++运算,将值1写入内存,这时g_val=1;
- 线程A由重新获取了CPU,进行++运算,得到的值为1,同样将值写回内存,此时仍是g_val=1;
- 故:我们最后的到的结果可能会有1
可以结合上面画的图在此理解一下上面的过程
线程安全:
要怎么才能保证线程安全呢?
我们需要用到互斥锁、同步变量保证互斥和同步的属性;
互斥:是指在某一时刻仅有一个执行流去访问临界资源
同步:保证执行流访问临界资源的合理性;
对应上面线程不安全的问题,即程序产生二义性,我们可以使用互斥锁来解决;
那么互斥锁是怎样保证原子性的呢?
互斥锁:
互斥锁在底层中是一个互斥量,本质上相当于一个计数器,仅有两个值即0或者1
取值为0的时候,就表示当前互斥锁已经被占用了,无法获取互斥锁,那么也就无法访问临界资源
取值为1的时候,就表示当前互斥锁没有被占用了,可以获取当前互斥锁,然后去访问临界资源
获取互斥锁称之为加锁,释放互斥锁称之为死锁;
对于一把互斥锁来说,如果有两个执行流加锁,则只能有一个执行流加锁成功,另一个执行流进行等待加锁(只有得到锁的执行流解锁后,该执行流才能加锁)
对于加锁、解锁来说:
加锁操作在底层中就是将互斥量中的计数器的值进行-1操作
解锁操作在底层中就是将互斥量中的计数器的值进行+1操作
对于互斥锁的加解锁操作而言,其实质也就是对一个变量进行加、减操作,从前面我们知道该操作并非是原子性的,那么互斥锁又是怎么保证线程安全的呢?
互斥锁原理:
对于互斥锁而言,其实加锁和解锁操作都是原子性的
为了保证这两个动作都是原子性的,实现了将寄存器和内存中的值进行互换;xchgb指令,该指令仅有一句,是一个原子操作;
详解:
在加锁的时候,会将寄存器中的值先置为0;
将寄存器中的值与计数器中的值进行互换 ;这个互换动作是原子性的
判断寄存器中的值,为1表示可以加锁,这把互斥锁被其他执行流占用
为0表示不可以加锁,得到当前锁资源
在解锁的时候,将计数器中的值置为1
我们可以写一段上面过程对应的伪码进行理解:
lock是指锁中的计数器; movb汇编指令: movb A B 表示将A值赋给B
加锁过程:
movb $0 %a1 xchgb %a1 lock if(寄存器a1的值>0) 可以加锁,获得锁资源 else 不可以加锁,锁被占用,执行流阻塞
解锁过程:
movb $1 lock
其实、互斥锁实现的目的就是达到互斥,即防止程序出现二义性、使用时就是用锁锁住临界资源,只有有锁的执行流才能访问临界资源,其余执行流都阻塞在加锁阶段,如下示意:
加锁
临界资源
解锁
而要保证线程安全还有另一个方面,即同步,保证程序出现结果的合理性即正确性
注:学习锁和同步变量的函数,可进入https://blog.csdn.net/weixin_43519514/article/details/105408649
同步:
线程不安全案例:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
#define PTHREADNUM 2
int apple=0;
pthread_mutex_t lock;
pthread_cond_t cond;
void* Consumer_pthread(void* arg)
{
(void*)arg;
while(1)
{
if(apple>0)
{
pthread_mutex_lock(&lock);
cout<<pthread_self()<<"我吃第"<<apple<<"个苹果"<<endl;
sleep(1);
--apple;
pthread_mutex_unlock(&lock);
}
}
}
void* Producer_pthread(void* arg)
{
(void*)arg;
while(1)
{
if(apple<=0)
{
pthread_mutex_lock(&lock);
++apple;
cout<<pthread_self()<<"新增苹果:"<<apple<<endl;
sleep(1);
pthread_mutex_unlock(&lock);
}
}
}
int main()
{
pthread_t consumer[PTHREADNUM];
pthread_t producer[PTHREADNUM];
pthread_mutex_init(&lock,NULL);
for(int i=0;i<PTHREADNUM;++i)
{
int ret=pthread_create(&consumer[i],NULL,Consumer_pthread,NULL);
if(ret!=0)
{
perror("pthread_create");
return -1;
}
ret=pthread_create(&producer[i],NULL,Producer_pthread,NULL);
if(ret!=0)
{
perror("pthread_create");
return -1;
}
ret=pthread_detach(consumer[i]);
if(ret!=0)
{
perror("pthread_detach");
return -1;
}
ret=pthread_detach(producer[i]);
if(ret!=0)
{
perror("pthread_detach");
return -1;
}
}
while(1);
pthread_mutex_destroy(&lock);
return 0;
}
上面的代码输出结果:
:
从上面的输出结果,我们可以看到,上面的程序存在着不合理性,没有苹果,顾客怎么能消费苹果?所以上面的程序存在着不安全的隐患;
而要解决这个问题,我们就须要用到条件变量,达到同步的效果;
而条件变量的应用于保证多个执行流的合理性
条件变量的原理:
两个接口(等待接口+唤醒接口)+PCB等待队列
条件变量的原理相对简单:
如果当前执行流请求不满足条件,调用等待接口,进入PCB等待队列;
如果其余执行流发现它满足条件的时候,调用唤醒接口,将其唤醒
注:学习锁和同步变量的函数,可进入https://blog.csdn.net/weixin_43519514/article/details/105408649
使用条件变量修正后的程序:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
using namespace std;
#define PTHREADNUM 2
int apple=0;
pthread_mutex_t lock;
pthread_cond_t cond;
void* Consumer_pthread(void* arg)
{
(void*)arg;
while(1)
{
pthread_mutex_lock(&lock);
while(apple<=0)
{
pthread_cond_wait(&cond,&lock);
}
cout<<pthread_self()<<"我吃第"<<apple<<"个苹果"<<endl;
sleep(1);
--apple;
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond);
}
}
void* Producer_pthread(void* arg)
{
(void*)arg;
while(1)
{
pthread_mutex_lock(&lock);
while(apple>0)
{
pthread_cond_wait(&cond,&lock);
}
++apple;
cout<<pthread_self()<<"新增苹果:"<<apple<<endl;
sleep(1);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond);
}
}
int main()
{
pthread_t consumer[PTHREADNUM];
pthread_t producer[PTHREADNUM];
pthread_mutex_init(&lock,NULL);
pthread_cond_init(&cond,NULL);
for(int i=0;i<PTHREADNUM;++i)
{
int ret=pthread_create(&consumer[i],NULL,Consumer_pthread,NULL);
if(ret!=0)
{
perror("pthread_create");
return -1;
}
ret=pthread_create(&producer[i],NULL,Producer_pthread,NULL);
if(ret!=0)
{
perror("pthread_create");
return -1;
}
ret=pthread_detach(consumer[i]);
if(ret!=0)
{
perror("pthread_detach");
return -1;
}
ret=pthread_detach(producer[i]);
if(ret!=0)
{
perror("pthread_detach");
return -1;
}
}
while(1);
pthread_mutex_destroy(&lock);
pthread_cond_destroy(&cond);
return 0;
}