• 引言

    惊群效应:当多个进程同事监听一个fd时,当fd相关的事件触发会唤醒多个进程去竞争该fd,但能获取到该事件的只有一个进程。如果我们使用mutex进行临界区保护的话,未抢到fd的进程就会睡眠。

    实际上,现在的linux内核已经处理accept()可能引起的惊群效应,但nginx为支持多平台,自己处理了可能触发的惊群效应。在nginx中,当事件触发时,多个进程会调用ngx_trylock_accept_mutex()尝试获取该事件。虽然nginx命名叫xxx_mutex(), 但实际上是由CAS操作实现的spin_trylock.即每个进程都尝试获取该事件。未能获取的就继续处理自己的事务。实际上尝试获取锁的代价仅仅是一个CAS汇编指令。

  • 调用栈

    ngx_trylock_accept_mutex()

    ngx_int_t
    ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
    {
        if (ngx_shmtx_trylock(&ngx_accept_mutex)) {
    
            ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                           "accept mutex locked");
    
            if (ngx_accept_mutex_held && ngx_accept_events == 0) {
                return NGX_OK;
            }
    
            if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
                ngx_shmtx_unlock(&ngx_accept_mutex);
                return NGX_ERROR;
            }
    
            ngx_accept_events = 0;
            ngx_accept_mutex_held = 1;
    
            return NGX_OK;
        }
    
        ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "accept mutex lock failed: %ui", ngx_accept_mutex_held);
    
        if (ngx_accept_mutex_held) {
            if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
                return NGX_ERROR;
            }
    
            ngx_accept_mutex_held = 0;
        }
    
        return NGX_OK;
    }
    

    ngx_shmtx_trylock()

    ngx_uint_t
    ngx_shmtx_trylock(ngx_shmtx_t *mtx)
    {
        return (*mtx->lock == 0 && ngx_atomic_cmp_set(mtx->lock, 0, ngx_pid));
    }
    

    ngx_atomic_cmp_set()

    static ngx_inline ngx_atomic_uint_t
    ngx_atomic_cmp_set(ngx_atomic_t *lock, ngx_atomic_uint_t old,
        ngx_atomic_uint_t set)
    {
        u_char  res;
    
        __asm__ volatile (
    
             NGX_SMP_LOCK
        "    cmpxchgq  %3, %1;   "
        "    sete      %0;       "
    
        : "=a" (res) : "m" (*lock), "a" (old), "r" (set) : "cc", "memory");
    
        return res;
    }
    

    最终就是CAS操作了。模型简化为,epoll唤醒多进程同时做CAS操作,成功的就处理该事件,失败的就继续处理自己的事务,惊群效应就这样完全在用户态解决了。


  • 总结

    其实在上述场景中,nginx是实现了spinlock的一部分,有兴趣的可以去深入了解下spinlock的实现细节,其中会涉及到中断处理和PAUSE指令的优化等。其实个人探究spinlock的出发点是,在我们的产品中有切换大流量分类DFA的需求,backgroud线程(生产者)会异步的构造这个DFA, 构造完成后就会赋值给临界区的指针变量。为了保证DFA规则的实时性和多线程的安全性。消费者每次在回调时做CAS操作尝试获取最新的DFA指针。我们知道指针的读取和赋值的时间是足够短的(几条汇编指令)。在不影响流量吞吐的情况下,我们也是采用了和nginx一样的做法。

    现在的mutex实现大都在用户态尝试几遍spinning,失败后才会去sleep,如果不是很确定spinning的时间足够短的话,就老老实实的用mutex吧。这里有关于怎样提高spinning的资料,详细参考:https://software.intel.com/en-us/articles/benefitting-power-and-performance-sleep-loops