线程安全
在了解线程安全之前我们需要先了解两个名词
- 临界资源:多线程执行流共享的资源
- 临界区: 线程内部访问临界资源的代码
线程安全是什么:多个执行流同时对临界资源争抢访问的操作不会造成数据二义性
线程安全的实现:
同步:通过条件判断保证对临界资源访问的合理性
互斥:通过同一时间对临界资源访问的唯一性实现对临界资源访问的安全性
线程安全互斥的实现
互斥:同一时间只有一个执行流访问资源,保证临界资源的安全性
互斥的实现:互斥锁
- 互斥锁:互斥锁是一个只有0/1的计数器,用于标记资源访问状态,在访问资源之前先访问互斥锁判断是否允许访问,不允许则使执行流阻塞,允许则让执行流访问临界资源并将资源设置为不可访问状态,访问完毕之后再恢复为可访问状态
互斥锁具体的操作流程一级接口介绍:
1.定义互斥锁变量
pthread_mute_t mutex;
2.初始化互斥锁变量(两种方法)
静态分配
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
//mutex:要初始化的互斥量
//attr:NULL
动态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
3.在访问临界资源之前进行加锁操作(不能加锁则等待,可以加锁则修改资源状态,然后调用返回,访问临界资源)
阻塞加锁–如果当前不能加锁(锁已经被其它加过了),则一直等待直到加锁成功调用返回
int pthread_mutex_lock(pthread_mutex_t *mutex);
非阻塞加锁–如果当前不能加锁,则立即报错返回 --EBUSY
int pthread_mutex_trylock(pthread_mutex_t *mutex);
挂起等待:将线程状态置为可中断休眠状态–表示当前休眠
被唤醒:将线程状态置为运行状态
4.在临界资源访问完毕之后进行解锁操作(将资源状态置为可访问,将其它执行流唤醒)
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
5.销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
注意:
- 1.互斥锁在创建执行流之前要初始化
- 2.在任何可能会退出执行流的地方进行解锁
- 3.所有的执行流都需要通过一个互斥锁实现互斥,意味着互斥锁本身就是一个临界资源,大家都会访问
如果互斥锁本身的操作都不安全如何保证别人安全所以互斥锁本身的操作必须是安全的
互斥本身的操作首先必须是安全的—互斥锁自身计数的操作是原子操作(一步操作完成)
线程安全同步的实现
通过条件变量实现
同步的实现:通过条件判断实现临界资源访问的合理性 – 条件变量
同步实现的原理:提供使执行流阻塞和唤醒的接口
当前是否满足访问资源的条件,若不满足,则让执行流等待,等到能够访问的时候再唤醒执行流
条件变量需要搭配互斥锁一起使用(信号量不需要)(判断条件是否满足的的条件本身就是一个临界资源,需要被保护,所以要搭配使用互斥锁)
因此条件的判断是需要进程自身进行操作,自身判断是否满足条件,不满足的时候调用条件变量接口使线程等待
使用接口的介绍:
1.定义条件变量
pthread_cond_t cond;
2.初始化条件变量
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t *attr);
pthread_cond_t cond = PTHREAD_COND_INITALLZER;
3.若资源获取条件不满足时调用接口进行阻塞等待(使线程挂起休眠的接口)
int 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_destory(ptread_cond_t *);
解锁与休眠操作是一步完成,保证原子操作
注意事项:
- 1.条件变量使用中对条件的判断应该使用while循环
- 2.多种角色线程应该使用多个条件变量
通过信号量实现
信号量:用于实现进程/线程间同步与互斥(主要用于同步)
信号量本质:信号本质就是一个计数器+pcb等待队列;
同步的实现
通过自身的计数器对资源进行计数,并且通过资源计数,判断进程/线程是否能够符合访问资源的条件,若符合就可以访问,若不符合则调用提供的接口使进程/线程阻塞;其它进程/线程促使条件满足之后,可以唤醒pcb等待队列上的pcb
互斥的实现
保证计数器不大于1,就保证了资源只有一个,同一时间只有一个进程/线程能够访问资源
代码操作:
1.定义信号量
sem_t
2.初始化信号量
int sem_init(sem_t *sem,int pshared, int value);
//sem:定义的信号量变量;
//pshared:0-用于线程间/非0-用于进程间
//value:初始化信号量的初值--初始资源数量有多少计数就是多少
3.访问临界资源之前,先访问信号量,判断是否能够访问,访问的话,计数-1
int sem_wait(sem_t *sem);
//判断自身计数是否满足访问条件,不满足则直接一直阻塞进程/线程
int sem_trywait(sem_t *sem);
//通过自身计数判断是否满足访问条件,不满足则立即报错返回,EINVAL
int sem_timewait(sem_ t *sem ,const struct timespec *abs_timespec);
//不满足则等待指定时间,超时后报错返回ETIMEDOUT
4.促使访问条件满足,计数+1,唤醒阻塞线程/进程
int sem_post(sem_t *sem);
//通过信号量唤醒自己阻塞队列上的pcb
5.销毁信号量
int sem_destory(sem_t *sem);
信号量与条件变量的区别:
1.条件变量需要搭配互斥锁一起使用,信号量不需要计数是原子操作
2.条件变量的条件判断需要外部操作,信号量通过自身计数完成
信号量与互斥锁的区别:
1.互斥锁的计数器只有0/1,信号量可以更多
2.信号量大多数情况下用来实现同步