线程安全
因为进程中线程共享了进程中的虚拟地址空间,所以线程间的通信更加方便,但数据有可能存在争抢关系,缺乏数据的访问控制,多个线程并发容易造成数据混乱,所以数据安全访问变得很重要。
造成数据混乱的的两个经典模型
同步与互斥概念
同步:线程/进程之间对临界资源的顺序访问关系(对临界资源访问的时序性)
互斥:线程/进程之间对临界资源的同一时间的唯一访问性关系
生产者与消费者模型
一个场所,两个角色,三种关系
生产者与生产者的关系:互斥(来保证数据的安全操作)
生产者与消费者的关系:同步和互斥
消费者与消费者的关系:互斥
如何来解决线程中数据的安全访问?------->实现线程间互斥
线程间的互斥实现:互斥锁(互斥量)
线程间的同步实现:条件变量
POSIX信号量:既可以实现同步可以实现互斥,既可以用于进程间的同步互斥,也可以实现线程间的同步互斥。
在互斥锁中死锁的必要条件?—如何避免
条件变量----等待和通知
为什么条件变量和互斥锁一起使用?
对于实现同步关键在于等待和通知,因为等待需要被唤醒,被唤醒的前提条件就是条件已经满足,并且这个条件本身就是一个临界资源。
互斥锁(或互斥量)-----实现线程间的互斥
互斥锁原理:
互斥锁以排他的方式防止共享数据被并发访问,是一个二元变量,
本质就是一个计数器,计数器只有0/1,在处理临界资源时要先申请互斥锁。互斥锁处于开锁状态,申请到互斥锁后立即占有该锁(加锁),防止其他线程访问资源。只有当前锁定该互斥锁的线程才可以释放该互斥锁。
互斥锁操作接口
1.定义一个互斥锁//在线程创建之前完成
定义一个互斥量(变量) pthread_mutex_t name
2.初始化互斥锁
互斥锁的初始化有两种方式:
1.定义时赋值初始化,不需要手动释放
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
2.函数接口初始化,需要手动释放
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
//参数一 : 互斥锁的变量
//参数二 : 互斥锁的属性,一般设置为NULL
返回0成功,返回非0就是错误
3.对临界操作进程加锁或解锁
加锁:
int pthread_mutex_lock(pthread_mutex_t* mutex);//阻塞式申请,如果锁被锁住则等待锁被打开,即若该锁是锁顶状态,默认阻塞当前进程。
int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞加锁,获取不到锁立即报错返回
int pthread_mutex_timedlock(pthread_mutex_t* restrict mutex, const struct timespec *restrict abs_timeout); //限时阻塞加锁,如果获取不到锁则指定等待时间,这段时间完了还没获取到,则报错返回
解锁(释放):int pthread_mutex_unlock(pthread_mutex_t *mutex);//在任意一个有任何可能性退出的地方都要解锁
4.销毁互斥锁
pthread_mutex_destroy(pthread_mutex_t *restrict mutex);
死锁情况:一直获取不到锁资源而造成的锁死情况
死锁产生的必要条件:必须具备以下条件才能满足
全部具备以下条件:
1.互斥条件----一个获取另外一个就不能获取
2.不可剥夺条件----一个线程获取锁只能由这个线程自己释放
3.请求与保持条件----获取第一个锁之后又去获取第二个锁
4.环路等待条件----a拿了锁1去申请锁2,而b拿了锁2去申请锁1,形成环路死锁
如何预防产生死锁:破坏死锁产生的必要条件
避免产生死锁:银行家算法(在这个算法中定义了两个状态,安全状态,非安全状态,如果某一步操作操作完毕后处于安全状态,那么可以执行,如果处于不安全状态那么就不能执行)
线性间互斥实例:
/*
这是一个买票的例子
每一个黄牛都是一个线程,在这个例子中有一个总票数ticket
* 每一个黄牛买到一张票这个ticket都会-1,直到票数为0
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
int ticket = 100;
//互斥锁的初始化有两种方式:
// 1. 定义时直接赋值初始化,最后不需要手动释放
// 2. 函数接口初始化,最后需要手动释放
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex; //定义互斥锁
void *y_cow(void *arg)
{
int id = (int)arg;
while(1) {
//2. 加锁操作
// int pthread_mutex_lock(pthread_mutex_t *mutex);
// 阻塞加锁,如果获取不到锁则阻塞等待锁被解开
// int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 非阻塞加锁,如果获取不到锁则立即报错返回EBUSY
// int pthread_mutex_timedlock (pthread_mutex_t *mutex,
// struct timespec *t);
// 限时阻塞加锁,如果获取不到锁则等待指定时间,在这段
// 时间内如果一直获取不到,则报错返回,否则加锁
pthread_mutex_lock(&mutex);
if (ticket > 0) {
usleep(100); //如果没有进行加锁操作,当票等于1时在睡眠的这个时间,很多线程都会进入,就会导致买到附属的票
printf("y_cow:%d get a ticket:%d!!\n", id, ticket);
ticket--;
}else {
printf("have no ticket!!exit!!\n");
//**加锁后,在任意有可能退出的地方都要进行解锁,
//**否则会导致其他线程阻塞卡死
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
//int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid[4];
int i = 0, ret;
//1. int pthread_mutex_init(pthread_mutex_t *mutex,
// const pthread_mutexattr_t *attr);
// 互斥锁的初始化
// mutex: 互斥锁变量
// attr:互斥锁的属性,NULL;
// 返回值:0-成功 errno-错误
pthread_mutex_init(&mutex, NULL);
for (i = 0; i < 4; i++) {
ret = pthread_create(&tid[i], NULL, y_cow, (void*)i);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
}
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
pthread_join(tid[2], NULL);
pthread_join(tid[3], NULL);
//4. 销毁互斥锁
pthread_mutex_destroy(&mutex);
return 0;
}
条件变量—实现线程间的同步
条件变量的原理:
互斥锁能够解决对资源的互斥访问,但有些情况互斥并不能解决
同步说的是对公共资源的时序访问,若有资源,则一个线程就会来访问,如果没有资源则线程就会等待,条件变量发生改变时就会进行通知,线程就会做相应工作。所以条件变量用于等待某个条件被触发。
在这里以生产消费者模型来详细说明一下同步:
条件变量不能单独使用,需要和互斥锁配合使用,因为线程等待被唤醒,被唤醒的前提是“条件改变了”,例如没有产品时 ,消费者等待,有产品时,消费者被唤醒,有无产品就是“这个条件”。
线程同步实现代码:
/* 这是一个实现生产者与消费者同步的代码,生产者消费者分别代表一个线程
* 有一个篮子,这个篮子是判断条件,
* 篮子里有面
* 代表消费者可以获取面,通知生产者面已经取走了
* 代表生产者需要等待
* 篮子里没有面
* 代表消费者等待
* 代表生产者放面,通知消费者面已经放了
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
//1. 定义条件变量
// 条件变量的初始化有两种方式
// 1. 定义赋值初始化,不需释放
// 2. 函数接口初始化, 需要释放
// pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond; //定义条件变量
pthread_mutex_t mutex; //定义互斥锁
int basket = 0;
//卖面的
void *sale_noddle(void *arg)
{
while(1) {
pthread_mutex_lock(&mutex); //basket就是是一个判断条件,线程都能访问
//比如有多个生产者时(多个线程),对于这一个公共数据,那么就会有争抢行为,需要互斥锁
if (basket == 0) { //加锁实现了对这个全局变量(公共资源的保护)
printf("sale noddle!!!\n");
basket == 1; //生产了面,然后开始通知对方,唤醒消费者,使其不再等待
//int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒所有等待在条件变量上的线程
//int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒第一个等待在条件变量上的线程
pthread_cond_signal(&cond);
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void *buy_noddle(void *arg)
{
while(1) {
pthread_mutex_lock(&mutex); //加锁保护
if (basket == 0) {
//没有面就要等待
//int pthread_cond_wait(pthread_cond_t *cond,
// pthread_mutex_t *mutex);第二个参数就是互斥锁
// pthread_cond_wait的功能就是用来阻塞等待某个条件变量。它做的事情就是先解锁然后进入等待
// pthread_cond_wait函数先对互斥锁做了一个判断是否加锁,如果加锁了就解锁
// 然后陷入等待*******整个过程是原子操作,不可被打断。
//
// 要防止的情况就是:假如没有面,而消费者又速度比较
// 快,先拿到锁了,那么生产者将拿不到锁,没法生产将会
// 造成双方卡死
// 所以如果消费者如果先获取到锁,那么在陷入等待之前需
// 要解锁
pthread_cond_wait(&cond, &mutex);
}
printf("buy noddles!!!\n");
basket = 0;
pthread_mutex_unlock(&mutex); //解锁
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
int ret;
//1. 条件变量的初始化
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
ret = pthread_create(&tid1, NULL, sale_noddle, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
ret = pthread_create(&tid2, NULL, buy_noddle, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//4. 条件变量的销毁
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
POSIX 标准信号量----既可以实现同步也可以实现互斥
即可用于进程也可以用于线程
POSIX信号量实现线程间的同步和互斥
**信号量本质:**具有一个等待队列的计数器
线程同步实现:
消费者:没有资源则等待
生产者:生产出来资源则通知等待队列中的等待者
/* 这是验证使用信号量还实现线程间同步与互斥的代码
信号量的操作步骤:
* 1. 信号量的初始化
* 2. 信号量的操作(等待/通知)
* 3. 信号量的释放
* 1. 同步:等待与通知
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <semaphore.h>
#include <pthread.h>
sem_t sem;
//线程间同步与互斥
void *thr_producer(void *arg)
{
while(1) {
//生产者
sleep(1);
printf("make a hot beef noddle!!\n");
//生产出资源后要通知等待在信号量上的线程/进程
//int sem_post(sem_t *sem);
//信号量修改的是自己内部的资源计数,这个内部的资源计数就是
//条件,而条件变量修改的是外部的条件,需要我们用户来修改
sem_post(&sem);
}
return NULL;
}
void *thr_consumer(void *arg)
{
while(1) {
//消费者
//2. 没有资源则等待
//阻塞等待,没有资源则一直等待有资源,否则获取资源
//int sem_wait(sem_t *sem);
//非阻塞等待,没有资源则报错返回,否则获取资源
//int sem_trywait(sem_t *sem);
//限时等待,没有资源则等待指定时长,这段时间内有资源则获取
//一直没有资源则超时后报错返回
//int sem_timedwait(sem_t *sem,struct timespec *timeout);
sem_wait(&sem);
printf("very good!!!\n");
}
return NULL;
}
int ticket = 100;
void *buy_ticket(void *arg)
{
while(1){
//大家都是黄牛!!
//因为计数器最大是1,也就代表只有一个线程能够获取到信号量
//这样也就保证了同一时间只有一个线程能操作
sem_wait(&sem);
if (ticket > 0) {
usleep(1000);
ticket--;
printf("cow %lu,buy a ticket:%d\n", ticket);
}
//操作完毕之后,对计数器进行+1,这时候信号量资源计数就又可
//以获取了,然后又进入新一轮的资源争抢,因为资源计数只有一
//个,因此也只有一个线程能够抢到
sem_post(&sem);
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
int ret;
//1. 初始化信号量
//int sem_init(sem_t *sem, int pshared, unsigned int value);
// sem:信号量变量
// pshared:
// 0-用于线程间
// 非0-用于进程间
// value:信号量的初始计数
ret = sem_init(&sem, 0, 1);
if (ret < 0) {
printf("init sem error!!\n");
return -1;
}
/*
//创建生产者线程
ret = pthread_create(&tid1, NULL, thr_producer, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
//创建消费者线程
ret = pthread_create(&tid2, NULL, thr_consumer, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
*/
//黄牛买票线程
pthread_t tid;
int i = 0;
for (i = 0; i < 4; i++) {
ret = pthread_create(&tid, NULL, buy_ticket, NULL);
if (ret != 0) {
printf("pthread_create error\n");
return -1;
}
}
pthread_join(tid, NULL);
//3. 销毁信号量
//int sem_destroy(sem_t *sem);
sem_destroy(&sem);
return 0;
}
资源争抢的另外一种模型------读写者模型(理解即可),实现读写模型的安全数据访问是用—读写锁
读写者模型:
大量读,少量写。
写的时候他人不能读,
读的时候不能写,
写的时候他人不能写,
读的时候他人可以读
互相关系 :
读写之间互斥
写于写之间互斥
读和读没有关系
读写锁的实现------读写锁了解即可