线程安全

线程安全:
因为进程中的线程共享了进程的虚拟地址空间,因此线程间通信将变得更加简单,但是缺点也随之而来:缺乏数据的访问控制容易造成数据混乱(因为大家都在争抢访问公共资源)。我们把能够造成数据混乱的情况总结了两个比较经典的模型,他们都是描述了多个进程/线程之间在数据访问时所应该保持的关系,不至于造成数据混乱和逻辑混乱。
这里写图片描述

//这是一个火车站黄牛买票的例子
//每一个黄牛都是一个线程,在这个例子中,有一个总票数ticket,一个黄牛抢到票,ticket-1
//直至ticket票数为0

#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>

int ticket=100;
void* route(void* arg)
{
        int tid=(int)arg;
        while(1)
        {   
                if(ticket>0)
                {   
                        usleep(1000);//当这个线程休眠时,可能会有其他线程进入该代码段
                        printf("%dbuy tickets:%d\n",tid,ticket);
                        ticket--;
                }   
                else
                {   
                        printf("no tickets\n");
                        pthread_exit(NULL);//线程退出
                }   
        }   
        return NULL;
}
int main()
{
        pthread_t tid1,tid2,tid3,tid4;
        //创建线程
        pthread_create(&tid1,NULL,route,(void *)1);
        pthread_create(&tid2,NULL,route,(void *)2);
        pthread_create(&tid3,NULL,route,(void *)3);
        pthread_create(&tid4,NULL,route,(void *)4);

        //线程释放资源
        pthread_join(tid1,NULL);
        pthread_join(tid2,NULL);
        pthread_join(tid3,NULL);
        return 0;
}

这里写图片描述
从运行结果看有的黄牛还买到-1张票,显然不对,那是为什么呢?
1.if 语句判断条件为真后,代码可以并发的切换到其他进程;
2.usleep这个模拟漫长业务的过程,在这个漫长的业务过程总,可能会有多个线程进入该代码段;
3.ticket–不是原子操作。
要解决以上问题,需要做到三点:
1.代码必须要有互斥行为:当代码进入临界区后,不允许有其他线程进入该临界区;
2.如果多个线程同时要求执行临界区代码,并且临界区没有线程在执行,那么只能允许一个线程进入临界区;
3.如果线程不在临界区中执行,那么该线程不能阻止其他线程进入该临界区。
要做到这三点,本质上是一把锁。linux上把这把锁称为互斥量。
这里写图片描述
如何实现线程间的互斥?

互斥锁:
定义一个互斥锁(即定义变量)
    pthread_mutex_t 是类型;
初始化互斥锁
互斥锁初始化有两种方式:
a:  定义时直接赋值初始化,最后不需要手动释放
pthread_mutex_t  mutex=PTHREAD_MUTEX_INITIALIZER;  //PTHREAD_MUTEX_INITIALIZER是宏
b:  函数接口初始化,最后需要手动释放(需要在线程创建之前)
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
功能:互斥锁初始化
mutex  :互斥锁变量
attr: 互斥锁的属性 ,NULL;
返回值:成功 :0   失败 :错误码   

对临界操作进行加锁/解锁
加锁:

  int pthread_mutex_lock(pthread_mutex_t *mutex);
  功能:阻塞加锁,如果获取不到锁阻塞,等待锁被解开(计数器变为1int pthread_mutex_trylock(pthread_mutex_t *mutex);
  功能:非阻塞加锁,如果获取不到则立即报错返回EBUSY
  int pthread_mutex_timedlock (pthread_mutux_t *mutex,struct timespec *t)
  功能:限时阻塞加锁,如果获取不到锁则等待指定时间,在这段时间内如果一直获取不到,则报错返回,否则加锁

解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
在加锁后任意有可能退出的地方都要进行解锁,否则会导致其他线程阻塞卡死

释放(销毁)互斥锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

那么由互斥锁可以将代码改为:

#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<pthread.h>
#include<stdlib.h>

int ticket=100;
pthread_mutex_t mutex; //定义互斥锁,线程都能访问到

void *y_cow(void *arg)
{
        int tid=(int )arg;
        while(1)
        {   
                //加锁
                //       int pthread_mutex_lock(pthread_mutex_t *mutex);
                pthread_mutex_lock(&mutex);
                if(ticket>0)
                {   
                        usleep(100);
                        printf("y_cow:%d get a ticket:%d\n",tid,ticket);
                        ticket--;
                }   
                else
                {   
                        //       int pthread_mutex_unlock(pthread_mutex_t *mutex);
                        printf("no tickets\n");
                        pthread_mutex_unlock(&mutex);
                        pthread_exit(NULL);
                }   
                pthread_mutex_unlock(&mutex);//需要在这里解锁,否则循环后由于没有解锁而阻塞等待
        }   
        return NULL;
}
int main()
{
        // int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);//初始化互斥锁
        pthread_mutex_init(&mutex,NULL);
        pthread_t tid[4];
        int i=0;
        int ret=-1;
        for(i=0;i<4;i++)
        {
                ret=pthread_create(&tid[i],NULL,y_cow,(void*)i);
                if(ret!=0)
                {
                        perror("pthread_create error");
                        return -1;
                }
        }
        pthread_join(tid[0],NULL);
        pthread_join(tid[1],NULL);
        pthread_join(tid[2],NULL);
        pthread_join(tid[3],NULL);
        return 0;
}

这里写图片描述
对互斥锁进行操作时,有加锁一定要有解锁,并且必须在任意一个有可能会退出的地方都要进行解锁操作,否则会造成其他线程的锁死。
死锁情况:因为一直获取不到资源而造成的锁死情况
死锁产生的必要条件:必须具备条件才能满足
1.互斥条件:我获取了你就不能获取
2.不可剥夺条件:我拿到的锁,别人不能释放我的锁,只能我释放
3.请求与保持条件 :拿了锁1又去获取锁2,没有获取到锁2不释放锁1
4.环路等待条件 :a拿了锁 1去请求锁2(没有获取锁2不释放锁1),b拿了锁2去请求锁1(没有获取锁1不释放2),a和b都在等待
预防产生死锁:破坏死锁产生的必要条件
避免产生死锁:银行家算法:
定义两个状态:安全状态
非安全状态
锁编号:大家都按这个编号来获取锁,以及释放
如何实现线程间的同步?
条件变量、posix信号量
条件变量:
1.定义初始化变量

条件变量初始化两种方式
a:  定义时直接赋值初始化,最后不需要手动释放
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
PTHREAD_COND_INITIALIZER是宏,
b:  函数接口初始化,最后需要手动释放(需要在线程创建之前)
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数:cond  要初始化的条件变量;
attr: NULL;

2.销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

3.等待:

int pthread_cond_wait(pthread_cond_t *restrict cond,  pthread_mutex_t *restrict mutex);
参数:cond 要在这个条件变量上等待
mutex: 互斥量,条件变量和互斥锁搭配使用

4.唤醒等待:通知线程有资源

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);
唤醒所有等待在条件变量上的线程  
int pthread_cond_signal(pthread_cond_t *cond);
唤醒第一个等待在条件变量上线程
功能:睡眠等到唤醒

为什么pthread_ cond_ wait 需要互斥量?
因为等待需要被唤醒,而被唤醒的前提条件是条件满足

1.条件等待是线程间同步的一种手段,如果只有⼀个线程,条件不满足,⼀直等下去都不会满⾜,所以必须要有⼀一个线程通过某些操作,改变共享变量,使原先不满⾜足的条件变得满⾜足,并且友好的通 知等待在条件变量上的线程。
2.条件不会⽆无缘⽆无故的突然变得满⾜足了,必然会牵扯到共享数据的变化。所以⼀一定要⽤用互斥锁来保 护。没有互斥锁就⽆无法安全的获取和修改共享数据。
3.按照上⾯面的说法,我们设计出如下的代码:先上锁,发现条件不满⾜,解锁,然后等待在条件变量 上,如下代码:

// 错误的设计  
  pthread_mutex_lock(&mutex);  
  while (condition_is_false) 
{        
    pthread_mutex_unlock(&mutex);        //解锁之后,等待之前,条件可能已经满⾜足,信号已经发出,但是该信号可能被错过     
   pthread_cond_wait(&cond);
   pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

条件变量使用规范:
等待条件代码

pthread_mutex_lock(&mutex);    
while (条件为假)        
pthread_cond_wait(cond, mutex);  
  修改条件  
pthread_mutex_unlock(&mutex);

给条件发送信号代码:

pthread_mutex_lock(&mutex);   
 设置条件真    
pthread_cond_signal(cond);    
pthread_mutex_unlock(&mutex);

这里写图片描述
posix标准信号量
posix标准信号量:既可以实现同步,也可以实现互斥
posix既可以用于进程间同步与互斥 ,也可以用于线程间同步与互斥。
信号量:具有一个等到队列的计数器
同步实现:
消费者:没有资源则等待
生产者:生产出资源则通知等待等待队列的等待者
下面为信号量实现线程间同步与互斥:
1.同步:等待与通知
信号量的初始化
信号量的操作(等待与通知)
信号量的释放
信号量的初始化

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
Link with -lrt or -pthread.
sem :信号量变量
pshared: 0代表线程间,非0代表进程间
value: 信号量的初始计数

信号量等待:

#include <semaphore.h>
int sem_wait(sem_t *sem);
     功能:阻塞等待,没有资源一直等待有资源;
int sem_trywait(sem_t *sem);
     功能:非阻塞等待,没有资源则报错返回,否则获取资源
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
     功能:限时等待,没有资源则等待指定时间,这段时间内有资源则获取,超时没有资源报错返回。
       Link with -lrt or -pthread.

信号量通知:

#include <semaphore.h>
int sem_post(sem_t *sem);
 Link with -lrt or -pthread.
//信号量实现线程的同步
//
include<stdio.h>
#include<pthread.h>
#include<error.h>
#include<semaphore.h>

sem_t sem; //全局信号量

void* thr_producer(void* arg)
{
        while(1)
        {   
                sleep(1);
                printf("product one\n");
                sem_post(&sem);
        }   
        return NULL;
}
void* thr_consumer(void *arg)
{
        while(1)
        {
                sem_wait(&sem); //没有资源则等待,判断是信号量内部自己判断
                //不需要手动修改计数器,是自己内部修改资源计数,这个内部资源计                    //数就是条件,而条件变量修改的是外部条件,需要用户自己修改
                printf("buy one\n");
        }
        return NULL;
}
int main()
{
        pthread_t tid1,tid2;
        //       int sem_init(sem_t *sem, int pshared, unsigned int value);
        //   pshared : 0代表线程间,非0代表进程间;value代表信号量初值
        sem_init(&sem,0,0);//在创建线程前 
        pthread_create(&tid1,NULL,thr_producer,NULL); //没有判断是否创建成功
        pthread_create(&tid2,NULL,thr_consumer,NULL);

        pthread_join(tid1,NULL);
        pthread_join(tid2,NULL);//等待线程退出
        sem_destroy(&sem); //销毁信号量

        return 0;  //如果没有pthread_join,return后进程退出,那么所有线程也就会退出,
}

这里写图片描述
信号量在线程间的互斥
计数器最大是1,也就代表只有一个线程可以获取到信号量,保证了同一时间只有一个线程能操作;
操作完毕后,对计数器+1,这时候信号量资源计数就可以获取,然后又进入新一轮资源争抢。

//信号量实现线程间互斥

#include<stdio.h>
#include<error.h>
#include<pthread.h>
#include<semaphore.h>  

sem_t sem;
int tickets=100;

void* thr_buy(void *arg)
{
        int i=(int)arg;
        while(1)
        {   
                usleep(1000);
                sem_wait(&sem);//等待
                if(tickets>0)
                {   
                        printf("%d buy tickets:%d\n",i,tickets);
                        tickets--;
                }   
                sem_post(&sem);//通知
        }
}
int main()
{
        pthread_t tid1,tid2,tid3;
        sem_init(&sem,0,1);
        pthread_create(&tid1,NULL,thr_buy,(void *)1);
        pthread_create(&tid2,NULL,thr_buy,(void *)2);
        pthread_create(&tid3,NULL,thr_buy,(void *)3);
        pthread_join(tid1,NULL);
        sem_destroy(&sem);
        return 0;
}

这里写图片描述
读写者模型:
少量写
大量读
这里写图片描述
写的时候不能读,读的时候不能写,一个人写的时候其他人不能写,一个人读的时候其他人也可以读;
读和写互斥(写优先:因为读的时候不能写,如果读的多的话,就可能一直没法写,因此写优先指:一旦要开始写,会等待当前正在读的人读完,但是如果有新的想读则不可以读,直至写完);写和写互斥;读和读没有关系。写独占,读共享,写锁优先级高。
如何实现读写者安全数据访问:读写锁。
读写锁初始化和销毁:

#include <pthread.h>
 int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
 int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

加锁和解锁:

加写锁:int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 
加读锁:int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
解锁: int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//1.读写锁的初始化
//2.读写锁的操作(加读锁,加写锁,解锁)
//3.读写锁的释放
//特性:写独占,读共享,写优先级高

#include<stdio.h>
#include<error.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>

pthread_rwlock_t rwlock;
int ticket=100;
void *thr_write(void *arg)
{
        while(1)
        {   
                //加写锁
                //       int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
                pthread_rwlock_wrlock(&rwlock); //写独占
                //       int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
if(ticket>0)
                //pthread_rwlock_rdlock(&rwlock);  //读共享
                if(ticket>0)
                {
                        usleep(1000);
                        ticket--;
                        printf("ticket:%d\n",ticket);
                }
                pthread_rwlock_unlock(&rwlock);
        }
}
int main()
{
        pthread_t wtid[4],rtid[4];
        int ret,i;
        //int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
        //const pthread_rwlockattr_t *restrict attr);
        pthread_rwlock_init(&rwlock,NULL);
        for(i=0;i<4;i++)
        {
               ret=pthread_create(&wtid[i],NULL,thr_write,NULL);
                if(ret!=0)
                {
                        printf("pthread_cteate error\n");
                        return -1;
                }
        }

        pthread_join(wtid[0],NULL);
        pthread_rwlock_destroy(&rwlock);
        return 0;
}

这里写图片描述
下面案例将用读锁和写锁:验证多个线程写和多个线程读 的特性。

//5个读线程,3个写线程
//
#include<stdio.h>
#include<pthread.h>
#include<error.h>

int counter;
pthread_rwlock_t rwlock;

void* thr_write(void *arg)
{
        int i=(int)arg;
        int t;
        while(1)
        {   
                usleep(1000);
                pthread_rwlock_wrlock(&rwlock); //写锁
                t=counter;
                printf("write:%d:%#X:counter=%d ++counter=%d\n",i,pthread_self(),t,++counter);
                pthread_rwlock_unlock(&rwlock); //写完之后就解锁
                usleep(4000);
        }   
}
void* thr_read(void *arg)
{
        int i=(int)arg;
        while(1)
        {
                pthread_rwlock_rdlock(&rwlock); //读锁
                printf("read:%d:%#X:count=%d\n",i,pthread_self,counter);
                pthread_rwlock_unlock(&rwlock);
                usleep(900);
        }


}
int main()
{
        pthread_t tid[8];
        int i=0;
        pthread_rwlock_init(&rwlock,NULL); //初始化读写锁
        for(i=0;i<3;i++)
        {
                pthread_create(&tid[i],NULL,thr_write,(void*)i);
 }
        for(i=3;i<8;i++)
        {
                pthread_create(&tid[i],NULL,thr_read,(void*)i);
        }
        for(i=0;i<8;i++)
        {
                pthread_join(tid[i],NULL);
        }
        pthread_rwlock_destroy(&rwlock);
        return 0;
}

这里写图片描述
上面案例验证只有当一个线程写完后,另一个线程才可以写,并且多个线程可以一起读,而且是写优先。
自旋锁
自旋锁一直轮询判断,这种做法十分 消耗cpu资源,因此适用于确定等待时间花费比较少,很快就能获取到锁的情况。
互斥锁是一直挂起等待。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值