概述
Postgresql中缓冲块的状态操作是非常频繁的,尤其是pin/unpin的操作。
这类操作类似于引用计数,具体由int类型记录:
- pin:
buf_state = BUF_USAGECOUNT_ONE
- unpin:
buf_state -= BUF_USAGECOUNT_ONE
这类 1的动作是concurrency unsafe的,需要使用锁或原子操作来保护。
(背景知识)
- PG的缓存页面每一个有8kB空间;对应的每一个页面都对应一个状态描述符BufferDesc。
- BufferDesc中记录缓存页面的信息,包括锁、引用计数、状态等等。
- PG对引用计数为0的buffer采用时钟算法淘汰
PinBuffer早期实现:spin lock
早期PG使用spin lock实现pin的自增操作,并发性能比较差。
代码语言:javascript复制PinBuffer
...
...
LockBufHdr(buf);
buf->refcount ;
...
...
if (buf->usage_count < BM_MAX_USAGE_COUNT)
buf->usage_count ;
result = (buf->flags & BM_VALID) != 0;
UnlockBufHdr(buf);
PinBuffer优化实现:CAS
使用CAS函数pg_atomic_compare_exchange_u32来做check&swap,兼顾原子性与性能(高并发readonly场景有8倍的性能提升)。
代码语言:javascript复制 old_buf_state = pg_atomic_read_u32(&buf->state);
for (;;)
{
if (old_buf_state & BM_LOCKED)
old_buf_state = WaitBufHdrUnlocked(buf);
buf_state = old_buf_state;
/* increase refcount */
buf_state = BUF_REFCOUNT_ONE;
/* increase usagecount unless already max */
if (BUF_STATE_GET_USAGECOUNT(buf_state) != BM_MAX_USAGE_COUNT)
buf_state = BUF_USAGECOUNT_ONE;
if (pg_atomic_compare_exchange_u32(&buf->state, &old_buf_state,
buf_state))
{
result = (buf_state & BM_VALID) != 0;
break;
}
}
- 整体流程是:
- 拿到state(包含引用计数)
- 增加引用计数、
- CAS检查拿到的state是否被并发修改
- 修改了:old_buf_state更新为最新的state,在最新的state基础上,再去增加引用计数。
- 没修改:old_buf_state更新为最新的state,buf->state更新为 后的state。
- 可以直观总结CAS的用法:
- 参数2总会更新为参数一的值,也就是拿到共享变量最新的状态。
- 参数1会将 检查和更新 合并为原子动作,如果检查参1==参2,则更新,更新后函数返回true。
- 参数1会将 检查和更新 合并为原子动作,如果检查参1!=参2,说明参1被并发修改,函数返回false。
PG的CAS函数惯用法:
代码语言:javascript复制原子函数拿到OLD
while(1)
用拿到的OLD做一些更新,记录到NEW,但不能修改OLD
if (pg_atomic_compare_exchange_u32(共享变量,OLD,NEW))
break;