线程安全 (这些接口都是C库实现的)
黄牛抢票(黄牛---线程, 票 --- 临界资源) 两个黄牛抢到同一张票,
1. 线程安全指的是多个线程同时运行访问临界资源, 不会导致程序的结果产生二义性,
临界资源: 在同一时刻, 该资源只能被一个执行流所访问, 涉及临界资源的区域 --> 临界区
访问: 在临界区当中对临界资源进行非原子操作,意味着可以被打断
(原子操作是一步完成的,当前操作只有两个结果, 要不完成, 要不未开始)
2. 如何保证线程安全
互斥: 保证在同一时刻只有一个执行流访问临界资源
互斥锁: (这个锁是在堆上开辟的, 而这个堆也是在PCB中的 属于task_struct结构体中 虚拟地址空间的堆)
!!!!!! 一定要在创建线程之前去初始化互斥锁, 这样就可以在线程内部使用互斥锁了, 当线程退出后, 需要释放互斥锁资源
!!!!!! 加锁一定是要在访问临界资源之前加的, 拿到锁资源之后, 再去访问临界资源
1. 使用互斥锁来保证互斥属性 (他维护他自己的计数器)
2. 互斥锁本质上是一个计数器, 但是这个计数器只有两个取值, 一个为0,一个为1,
0代表: 无法获取互斥锁
1代表: 可以获得互斥锁
3 在访问临界资源前, 先获取互斥锁
如果能够获取到互斥锁, 表示当前资源可以去访问
如果不能获取到互斥锁, 表示当前资源不可以去访问, 也就是意味着计数器当中的值为0, 意味着已经有一个线程正在访问临界资源
4. 使用流程:
1. 初始化互斥锁
2. 加锁(将计数器当中的值变成0)
如果计数器为1, 可以正常加锁
如果计数器为0, 不可以正常加锁, 当前想要加锁的执行流被阻塞
3. 访问临界资源
4. 解锁(将计数器当中的值变成1)
5. 操作接口
1. 定义互斥锁
pthread_mutex_t:互斥锁变量类型
2. 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t* mutex, pthread_mutexattr_t* attr) (动态初始化)
mutex: 互斥锁变量, 要初始化哪一个互斥锁变量, 一般情况下, 定义互斥锁对象, 传入互斥锁对象的地址
attr: 互斥锁的属性, 一般情况下, 我们直接置为NULL, 采用默认属性
课后调研: 为什么可以直接这样赋值就初始化了????????????????? 查看源码 查看定义
还有一种初始化方式为赋值初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER(也叫静态初始化)
3. 加锁
int pthread_mutex_lock(pthread_mutex_t* mutex) ----> 阻塞加锁操作
mutex: 要对哪一个互斥锁进行加锁, 传递互斥锁对象的地址即可
该接口为阻塞接口, 如果加锁成功则返回, 如果加锁失败, 直到加锁成功
这是一个带有超时的加锁接口
int pthread_mutex_timedlock(pthread_mutex_t* mutex, const struct timespec* abs_timeout)
mutex: 要对哪一个互斥锁进行加锁, 传递互斥锁对象的地址即可
abs_timeout: 加锁超时时间, 在加锁的时候, 如果超过超时时间还没有加上锁, 则直接返回, 会报错, 报错TIMEOUT, 不会进行阻塞等待
struct timespec: 有两成员, 第一个变量为秒级, 第二个为纳秒
int pthread_mutex_trylock(pthread_mutex_t* mutex) ----> 非阻塞加锁操作
mutex: 传入互斥锁的地址, 来进行加锁操作
如果计数器值为1, 意味着可以加锁, 加锁操作后, 对计数器当中只从1改变成为0
如果计数器值为0, 一位这不可以加锁, 该接口直接返回, 不进行阻塞等待, 返回EBUSY, 表示拿不到加锁资源
4. 解锁操作:
int pthread_mutex_unlock(pthread_mutex_t* mutex)
不管使用pthread_mutex_lock/pthread_mutex_trylock/pthread_mutex_timedlock三种哪种接口加锁, 都可以使用unlock接口都可以解锁
如果不解锁, 导致阻塞等待锁资源 , 不解锁就会使执行流陷入阻塞, 在二次对互斥锁加锁时, owner线程也进入等待
thread apply all bt: 调试的时候使用这个命令 可以查看所有线程的调用堆栈
如果不释放互斥锁, 则会阻塞其他线程, 我们称之为死锁
互斥锁只能被自己线程(谁拿到锁资源)释放, 别的线程都释放不了
什么时候解锁呢???????????????????????????????????????????????????????
若只在入口函数退出是加入解锁, 导致线程用完互斥锁之后, 程序执行后, 第一个工作线程把锁拿走了, 未解锁, 且退出了(类似偷走了), 其他线程进行不了解锁操作, 计数器的值依旧为0, 导致阻塞
所以应在所有可能退出的地方都要加上解锁操作,
5. 销毁互斥锁变量
int pthread_mutex_destory(pthread_mutex_t* mutex)
CPU是操作系统去管理的, 其他程序分时间在CPU上运行
问题: 对于加锁操作, 将计数器当中的值从1变成0, 为什么是原子操作?
这个互斥锁的计数器本身就是C库在维护的, 计数器本身也是一个变量, 这个变量保存了1/0; 根据冯诺依曼体系结构, 内存和CPU, 寄存器的关系, 进行加锁时 需要CPU去判断是否可以加锁, 计数器(mutex_val)的值保存在内存中, 不管计数器的值是怎样, 只有加锁都给寄存器中放一个0, 加锁时交换寄存器与内存的值, xchgb(汇编中的指令), 这个交换操作是原子性的, 在交换结束后, 接下来只需要去
判断寄存器中的值是否为1, 来判断是否可以加锁,
如果寄存器中的值为1, 则表示之前内存中计数器的值为1, 表示可以加锁,
如果为0, 则表示之前内存中计数器的值为0, 表示不可以加锁;
主要是由于这个交换操作保证了原子性.
上锁: 第一步是将寄存器的值变为0, 第二步将寄存器当中的值与内存的值交换, 接着判断寄存器当中的值是否大于0, 如果大于0 , 直接返回, 说明lock这个函数返回了, 当等于0时 进行上锁, 当寄存器中的值为0时, 上锁成功
解锁时, 将内存中计数器的值变为1 (每句汇编代码都保证了原子性, 但是我们所写的指令则可能是多条汇编代码, 而被调度的时候是一条指令还执行完, 返回时从上次中断的汇编代码的下一条代码开始 恢复现场)
死锁:
1. 什么是死锁?
程序当中有哟个执行流没有释放锁资源, 会导致其他想要获取该所的执行流陷入阻塞等待, 这种情况被称为死锁
程序当中每一个执行流都占有一把互斥锁, 但是由于各个执行流在占有互斥锁(已经获取)的情况下, 还想申请对方的锁(其他执行流所占有的互斥锁), 这种也会导致死锁 ------ 吃着碗里的,看着锅里的, 谁申请谁阻塞
条件变量 --> 同步: 保证程序对临界资源访问的合理性
信号量 --> 既能保证同步 也能保证互斥