用原子操作实现读写锁——nginx_rwlock代码分析

1,462 阅读2分钟

一、背景

在日常性能优化中,经常有同学会考虑使用无锁机制来减少 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;
    }
}