线程安全
线程安全是指多个线程执行流对同一临界资源进访问时, 存在竞争关系,会造成临界资源的数据二义性
互斥
线程之间保证同一时间只有一个线程访问临界资源
- 互斥锁与其实现原理
互斥锁的实现就是通过只有 0/1的计数器来实现两种状态, 要么能访问, 要么不能访问
由于对计数器进行加减的操作是非原子性的,所以他不能保证安全, 则就使CPU中的寄存器置为0,
//互斥锁的定义
pthread_mutex_t mutex;
//互斥锁的初始化 , 第二个参数为互斥锁的属性,通常置空
pthread_mutex_init(&mutex,NULL);
//加锁
pthread_mutex_lock(&mutex);
//解锁
pthread_mutex_unlock(&mutex);
//互斥锁的销毁
pthread_mutex_destory(&mutex);
下面通过一个黄牛抢票的程序体现互斥锁在线程安全中的作用
int num=100;
void* yellow_cow(void* arg){
int* id=(int*)arg;
while(1){
if(num>0){
num--;
printf("黄牛%d号抢到了一张票-----票号:%d",id,num);
}else{
pthread_exit(NULL);
}
}
}
int main(){
pthread_t tid[4];
int ret=0;
for(int i=0;i<4;i++){
ret=pthread_creat(&tid[i],NULL,yellow_cow,(void*)i);
if(ret!=0){
printf("创建线程失败!!\n");
return -1;
}
}
for(int i=0;i<4;i++){
pthread_join(&tid[i],NULL);
}
return 0;
}
在上述代码中, 四个线程代表着四个黄牛, 分别对一百张票进行抢夺, 由于有多个执行流对一个资源进行访问,因此票数num是一个临界资源, 四个线程对其进行访问的时候,就会造成数据的二义性.
因此为保障临界资源的数据安全, 对临界资源操作的时候,就要使用互斥锁,代码如下述所示
pthread_mutex_t mutex;
int num=100;
void* yellow_cow(void* arg){
int id=(int)arg;
while(1){
pthread_mutex_lock(&mutex);
if(num>0){
printf("抢到票了!!!-----票数: %d------黄牛id: %d\n",num,id);
--num;
usleep(10000);
}
else{
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
pthread_mutex_unlock(&mutex);
}
}
int main(){
int i=0,ret;
pthread_t tid[4];
//互斥锁的初始化
pthread_mutex_init(&mutex,NULL);
for(i=0;i<4;i++){
ret=pthread_create(&tid[i],NULL,yellow_cow,(void*)i);
if(ret!=0){
printf("线程创建失败!!\n");
return -1;
}
}
for(i=0;i<4;i++){
pthread_join(tid[i],NULL);
}
//互斥锁的销毁
pthread_mutex_destroy(&mutex);
return 0;
}
在创建线程之前创建互斥锁, 在线程结束之前销毁互斥锁
将加锁的操作放在循环之内, 当四个线程进入循环之后, 时间片快的线程,假设线程A首先加锁, 其他的三个线程就会阻塞在加锁的操作之上, 当线程A对临界资源操作完成之后就会解锁, 之后下一个线程B的时间片来临, B线程加锁, 其他的三个线程又阻塞在加锁操作之上. 四个线程会一直按照此操作直到线程终止
但是要注意: 以上述代码为例, 当线程A对临界资源操作完成之后, 恰好此时的NUM已经为0, 线程就会退出, 而其他的三个线程就会一直卡在加锁的操作上, 因此在程序中只要有线程终止的地方都要先进行解锁操作, 之后再退出线程
但是还存在一个问题,如果程序执行的速度非常快, 线程A在自己时间片中完全有可能将num的资源全部抢光. 因此互斥锁只能实现线程安全中的互斥, 而不能报纸同步
同步
线程之间对临界资源访问的时序合理性,它主要体现过在,当线程不能对当前临界资源进行操作的时候,让线层陷入等待, 当满足了对临界资源访问的条件时,则唤醒线程
条件变量及其实现原理
//定义条件变量的类型
pthread_cond_t cond;
条件变量的初始化与销毁
//条件变量的初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
//条件变量的销毁
int pthread_cond_destroy(pthread_cond_t *cond);
第一个参数为条件量的名称
第二个参数为条件变量的属性–通常置为NULL
条件变量的等待
每一个条件变量的定义都对应一个等待队列 , 等待的操作就在等待队列上进行
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
//限时等待, 规定扥等待一定的时间,时间到后若没有被唤醒,则报错返回
int pthread_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
其中一共有三个参数:
第一个为需要等待的条件变量的名称
第二个为互斥锁
第三个为规定的等待时间
注意 : 为何条件变量中需要互斥量?
由于条件变量并没有提供判断条件的功能,多以临界的条件变量判断量以及判断过程都需要用户自己来定义与判断.
当我们的条件变量阻塞等待其满足操作临界资源的条件时,就需要有其他的线程开改变从而达到满足访问的条件,那么在这个过程中就有了对共享变量的访问,就需要互斥锁保护
所以当我们的某一个线程要对条件变量进行判断时,先加锁 , 但是当该线程不满足访问临界资源时 , 他会陷入等待,由于前一步加锁, 其他的线程,无法改变条件变量,使该线程满足访问临界资源的条件, 所以在等待之前需要进行一步解锁 ; 但是等待和解锁的过程是非原子操作的,因此大佬们将这两步操作封装在了一个接口里.
条件变量的唤醒
事先我们说了, 当某一个线程不满足对临界资源的访问条件时, 该线程就会等待在条件变量的等待队列上, 当另一个线程进行一系列的操作之后,是上一个线程达到了访问临界资源的条件, 则就需要对该线程进行唤醒.
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
唤醒线程的参数只有一个,就是用户要唤醒那个条件变量的等待队列上的线程
而pthread_cond_broadcast接口是广播唤醒-----广播唤醒的意思就是,同一时间唤醒所有的线程.
下面以一个吃面者与做面者的例子说明条件变量
//定义判断条件变量的参数
int is_heave_noodles=0;
//定义吃面者变量与做面者变量
pthread_cond_t cook_cond;
pthread_cond_t eat_cond;
pthread_mutex_t mutex
void thr_cook(int *arg){
while(1){
pthread_mutex_lock(&mutex);
while(is_have_noodles==0){
pthread_cond_wait(&eat_cond,&mutex);
}
cout<<"吃面者吃了一碗面"<<endl;
is_have_noodles=0;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cook_cond);
}
return NULL;
}
void thr_eat(int *arg){
while(1){
pthread_mutex_lock(&mutex);
while(is_have_noodles==1){
pthread_cond_wait(&cook_cond,&mutex);
}
cout<<"做面者做了一碗面"<<endl;
is_have_noodles=1;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&eat_cond);
}
return NULL;
}
int main(){
int i,ret=0;
pthread_t ctid[4];
pthread_t etid[4];
pthread_cond_init(&cook_cond,NULL);
pthread_cond_init(&eat_cond,NULL);
pthread_mutex_init(&mutex,NULL)
for(i=0;i<4;i++){
ret=pthread_create(&cpid[i],NULL,thr_cook,NULL);
if(ret!=0){
cout<<"线层创建失败!!"<<endl;
return -1;
}
}
for(i=0;i<4;i++){
ret=pthread_create(&epid[i],NULL,thr_eat,NULL);
if(ret!=0){
cout<<"线层创建失败!!"<<endl;
return -1;
}
}
for(i=0;i<4;i++){
pthread_join(ctid[i],NULL);
pthread_join(etid[i],NULL);
}
pthread_cond_destroy(&cook_cond);
pthread_cond_destroy(&cook_cond);
pthread_mutex_destroy(&mutex);
return 0;
}
注意:
在对条件进行判断的时候, 需要记性循环判断,防止不满足条件的线程被唤醒, 从而导致直接对临界资源进行操作, (因为当有多个吃面的执行流进入到吃面操作的时候, 会对锁资源进行竞争, 当有一个锁竞争上的时候, 其他的线程就会阻塞在锁上, 等待解锁, 当这一线程操作完临界资源之后,就会唤醒做面者线程, 在唤醒前会进行解锁, 而以前阻塞在加锁位置的吃面者线程也会在这一时刻竞争锁资源, 倘若竞争上了,就不会对条件进行判断)
不同角色的线程需要等待到不同的条件变量上, 如果等待到同一个等待队列上, 唤醒是,就会造成歧义