MDL_map_partition中对锁的过渡
Sat, Apr 5, 2014在MDL
源码中有一段MDL_map_partition
中对锁的过渡有点意思, 拿出来分析一下
场景
MDL_map_partition
是容纳MDL_lock
的一个容器, MDL_lock
可以简单的理解为一种锁.
那么场景问题是现在我要从锁容器C
中查找一个锁L
并加锁L
, 怎样做到多线程安全
场景中C
上有保护C
的锁A
(容器锁), L
上的锁为B
(成员锁) (此处做了简化, 实际上MDL_lock
并不是一个锁, 而是类似于条件变量, 而锁B
是保护L
的锁. 此处将L
简化为一把锁)
分析1
最简单的方法, 就是A
加锁, B
加锁, A
解锁. 这种方法肯定不会有错, 但是并发性上会出现问题. 比如以下场景
- 线程T1持有
B
- 线程T2正在容器中查找
B
. - 线程T3在同一个容器中查找另外一个
MDL_lock
T2先将A
加锁, 加锁B
时, 由于T1持有B
, T2被阻塞; T3在同一个容器中查找另一个不相关的成员, 先要加锁A
, A
被T2持有, T3被阻塞
因此, 这种做法的并发性很差
分析2
提高并发性的关键是将A
锁过渡到B
锁, 比如这样: A
加锁, 查找B
, A
解锁, B
加锁.
这种方法解决了并发性, 但显而易见形成了一个无锁区 (从A
解锁到B
加锁这个区域). 如果在无锁区另一个线程将B
销毁或移出容器, 那么后面的B
加锁操作就会悲剧
分析3
面对无锁区的问题, 可以试着加version
(版本变量)来解决, 规则如下:
- 任何将成员移入/移出容器的情况, 都需要获得容器锁
A
和成员锁B
, 并在元素version
上加1 - 对成员的销毁, 需要先将成员移出容器
这样, 查找成员的流程变为:
- 线程T1, 对
A
加锁, 找到B
, 记录B
的version
, 记为v1. 对A
解锁 - 线程T2,
B
销毁或移出容器, 需要获得A
和B
锁, 对version
加1, 记为v2 - 线程T1, 等到T2释放
B
锁后, 可获得B
锁, 发现v1 != v2
, 意味着成员可能在容器中已经被移出或销毁, 则需要重试整个过程
加入version
后, 对于销毁成员的场景, 并发性并没有改变 (因为仍然需要同时获得两把锁), 但对于查找成员的场景, 并发性和分析2一样
不幸的是, 这个场景仍然存在问题, 很容易看到其中一个逻辑问题, T1在T2销毁B
锁后, 还获得了B
锁. 也就是T2不能即刻销毁B
锁, 否则所有等待B
锁的线程都会悲剧. 那B
锁何时能被安全销毁
分析4
要解决分析3的问题, 可以在B
上添加引用计数, 细节如下:
- 在成员未被移出容器时, 持有
A
锁可以对成员引用计数usage_count
进行加1, 即在容器中查找成员时, 容器负责对成员的usage_count
加1 - 持有
B
锁可以对自己的解引用计数release_count
进行加1, 即使用者在使用完B
后, 对B
进行解引用 - 如果
usage_count
==release_count
, 则B
可以被安全销毁
可以看到usage_count
和release_count
在分别在不同锁的保护下, 代入分析3的场景, 发现可以解决分析3的问题
还有一些需要说明的边界情况
- 在成员已经被移出容器后, 成员引用计数
usage_count
不再受A
锁保护, 而是受B
锁保护. 相当于容器已经不再管理成员的引用计数 - 如何判断”成员已经被移出容器”, 可以在成员上添加状态量
is_removed_from_container
, 读取此状态需要A
锁或B
锁, 修改此状态需要A
锁和B
锁.
Mysql的实现
Mysql的实现和之前的分析大致相同, 给出映射表
分析里的概念 | Mysql的变量 |
---|---|
版本变量version |
MDL_lock.m_version |
成员引用计数usage_count |
MDL_lock.m_ref_usage |
成员解引用计数release_count |
MDL_lock.m_ref_release |
状态量is_removed_from_container |
MDL_lock.m_is_destroyed |
实现锁拆分的函数为MDL_map_partition::move_from_hash_to_lock_mutex
, 一看就懂