一、背景
在日常性能优化中,经常有同学会考虑使用无锁机制来减少 overhead,但无锁机制涉及很多操作系统实现细节(cache 一致性等),用错 API 或逻辑不严密很容易导致一些难以复现的 bug,今天我们借 nginx 中封装的无锁机制(基于CAS),以读写锁为例来学习一下如何使用无锁机制。
二、CAS 实现读写锁具体机制
1. 读写锁的对外接口:
//ngx_rwlock.h
void ngx_rwlock_wlock(ngx_atomic_t *lock);
void ngx_rwlock_rlock(ngx_atomic_t *lock);
void ngx_rwlock_unlock(ngx_atomic_t *lock);
-
其中:ngx_atomic_t 及基础 CAS 操作在 Darwin 下的定义为:
//os/unix/ngx_atomic.h #include <libkern/OSAtomic.h> typedef int64_t ngx_atomic_int_t; typedef uint64_t ngx_atomic_uint_t; typedef volatile ngx_atomic_uint_t ngx_atomic_t; //Darwin系统下的基本 CAS 操作 #define ngx_atomic_cmp_set(lock, old, new) \ OSAtomicCompareAndSwap64Barrier(old, new, (int64_t *) lock) #define ngx_atomic_fetch_add(value, add) \ (OSAtomicAdd64(add, (int64_t *) value) - add) #define ngx_memory_barrier() OSMemoryBarrier() #define ngx_cpu_pause() -
另外一些基础操作定义:
//os/unix/ngx_process.h #if (NGX_HAVE_SCHED_YIELD) #define ngx_sched_yield() sched_yield() #else #define ngx_sched_yield() usleep(1) #endif
2. 具体实现
- 锁本身就是一个 uint64_t 类型的变量
- 读锁操作定义为给锁值增加 1 —— 代表增加一个读者
- 写锁操作定义为将锁值赋为 -1 具体逻辑见代码注释:
//控制自旋次数
#define NGX_RWLOCK_SPIN 2048
//写锁,定义为将锁值置为-1
#define NGX_RWLOCK_WLOCK ((ngx_atomic_uint_t) -1)
//加写锁
void ngx_rwlock_wlock(ngx_atomic_t *lock)
{
ngx_uint_t i, n;
for ( ;; ) {
//之前未加锁且成功 CAS
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, NGX_RWLOCK_WLOCK)) {
return;
}
if (ngx_ncpu > 1) {
//如果有多个cpu,则立即重试十次
for (n = 1; n < NGX_RWLOCK_SPIN; n <<= 1) {
//逐次增加pause时间
for (i = 0; i < n; i++) {
ngx_cpu_pause(); //高版本gcc里会转化为 __asm__("pause"),其他无效果
}
//只有没有读者时才能加写锁,使用CAS机制写NGX_RWLOCK_WLOCK即-1到锁变量中
if (*lock == 0
&& ngx_atomic_cmp_set(lock, 0, NGX_RWLOCK_WLOCK))
{
return;
}
}
}
//重试十次失败(或只有一个cpu),sleep让出cpu
ngx_sched_yield(); //usleep(1)
}
}
//加读锁
void ngx_rwlock_rlock(ngx_atomic_t *lock)
{
ngx_uint_t i, n;
ngx_atomic_uint_t readers;
for ( ;; ) {
readers = *lock;
//无写锁,且锁未被其他读者更改,则直接加读锁,锁值+1
if (readers != NGX_RWLOCK_WLOCK
&& ngx_atomic_cmp_set(lock, readers, readers + 1))
{
return;
}
if (ngx_ncpu > 1) {
//如果有多个cpu,则立即重试十次
for (n = 1; n < NGX_RWLOCK_SPIN; n <<= 1) {
//逐次增加pause时间
for (i = 0; i < n; i++) {
ngx_cpu_pause(); //高版本gcc里会转化为 __asm__("pause"),其他无效果
}
//读取锁内容
readers = *lock;
//未加写锁且用 CAS 判断锁未被其他读锁更改则成功增加读者
if (readers != NGX_RWLOCK_WLOCK
&& ngx_atomic_cmp_set(lock, readers, readers + 1))
{
return;
}
}
}
//重试十次失败(或只有一个cpu),sleep让出cpu
ngx_sched_yield(); //usleep(1)
}
}
//解锁
void ngx_rwlock_unlock(ngx_atomic_t *lock)
{
ngx_atomic_uint_t readers;
readers = *lock;
//处理写锁的解锁,下一步的 CAS 赋0之前别的线程无法加写锁也无法加读锁,所以这里不用自旋
if (readers == NGX_RWLOCK_WLOCK) {
(void) ngx_atomic_cmp_set(lock, NGX_RWLOCK_WLOCK, 0);
return;
}
//对读锁来说因为还有可能别的线程还在并行地加读锁,这里需要自旋进行处理
for ( ;; ) {
if (ngx_atomic_cmp_set(lock, readers, readers - 1)) {
return;
}
readers = *lock;
}
}