摘要:
使用锁来控制并发, 既熟悉,又陌生。锁的使用再熟悉不过了,用锁来控制并发就像用锁来处理多线程的安全性问题一样,mysql/innodb的所谓x锁/s锁/间隙锁等等以及什么时候锁表什么时候又是行锁又是八股文里的常客。很难对利用锁来做并发控制不产生一种他乡遇知音的熟悉感。
但是这只是肤浅的表面认知,仅仅是流于表面,对并发控制以及使用锁来进行并发并没有深刻的以及系统化的思考。
本文对利用锁进行并发控制做一些思考。
上下文参考: 2023-08-30 数据库-并发控制-冲突可串行化调度-是否可串行化检测-优先图-分析_财阀悟世的博客-CSDN博客
对于锁的理解:
- 锁是什么? 这个问题要是深入的想想,很奇怪对不对,硬件层面只有中断,没有线程和锁的硬件层面的组成!线程和锁是操作系统抽象出来的
- linux中对于线程和进程都是task,也就是调度的一个单元, 区别在于内存可见性, 同一个进程的线程共享堆空间, 但是进程间地址空间完全隔离
- 这里就引发出进程间通信和线程间通信,进程间通信一般是网络socket, 共享内存,消息队列,父子进程间的copy on write. 例如redis的后台持久化就是创建一个子进程
- 线程间的通信一般是叫做线程安全, 由于进程的内存直接可见, 所以直接通信,但是这里就存在多个线程并发执行,线程可访问到对方线程的数据, 导致互相影响
- 为了控制临界区,搞出了锁这个概念
- 那么锁是什么?
- 直白点说,锁其实是个全局的变量, 每个线程都能访问到
- 持有锁, 换句话说,就是修改这个变量。检测是否可以获取锁, 其实就是判断这个变量是否已经被修改过, 如果被修改过, 那么就是被其他线程持有了, 本线程只能等待.
- 所以这就可以理解了,锁是个变量,来控制线程是否能继续往下执行, 也就是所谓的临界区, 处于等待状态的线程, 不断的周期性的判断变量的值, 以此来作为锁是否已经被其他线程释放,可以被本线程持有
- linux中的线程实现pthread是一个在应用层的类库, 是用户态的概念, 所谓的互斥锁mutex,读写锁,信号量等等是在用户态实现
- 无锁队列是什么? 什么叫无锁? 其实就是使用CAS来在用户程序内部来实现比较和修改这种原子逻辑, 如果compare不符合预期那么就用个while继续比较判断, 类似于原子锁, 这样线程就不陷入睡眠状态, 看起来就无锁了
用锁来做并发控制:
- 第一个也是最重要的问题, 锁究竟是为了保护什么? 肤浅的回答是为了保护临界区, 但是临界区又是什么?
- 是的, 锁真正所要保护的, 是数据!是对数据的变化, 也就是写/修改数据, 因为只有写会导致其他线程对数据读取失效,锁保护的是数据,而不是行为
- 那么在数据库领域, 数据是什么? 说数据容易引起误解, 一般称呼为数据库资源
- 那么数据库的资源又是什么? 保护的是什么? 如果将表理解为资源的单元,那么就是表锁;如果将行理解为资源的单元,那么就是行锁;如果将磁盘块和数据页当作资源的单元,那么就是页锁;
- 和事物结合起来, 事物其实可以理解成对资源进行读和写操作的序列, 那么锁是对资源的保护, 那么在修改资源前, 就要加锁, 修改资源后,就要释放锁
利用锁做并发控制的原则:
不但要考虑锁的使用原则,也要考虑事物调度本身的特性
一. 事物的一致性
- 持有锁的线程, 负责释放锁
- 如果一个事物要修改某些数据, 必须要在修改数据前加锁,在修改数据后解锁
二.调度的合法性
- 锁必须具有语义,两个事物不能同时对同一个数据库资源加锁
- 也就是说,一个事物要将获得对某个数据库资源的修改,那么必须等待其他事物先释放锁
利用锁进行并发控制的抽象动作计法:
- 如果一个没加锁的事物是:T=R(A),W(A),R(B),W(B),R(B)
- 那么设定加锁的符号为l,解锁的符号为u
- 进行加锁则为: T = L(A), R(A), W(A), U(A), L(B), R(B), W(B), R(B), U(B)