perfbook 读书笔记:Non-Blocking Synchronization

perfbook 中对 NBS 这一节的描述尚未完成。我来简单补充一下。

Non-Blocking 其实很简单,它和 Blocking 相反,也就是非阻塞的意思。阻塞大家都知道,典型的 mutex,semaphore 就是阻塞型的锁,如果其它线程持有锁,该线程会进入阻塞状态,直到它可以获取锁。

非阻塞型的锁就是竞争时不会阻塞的锁,最典型的就是家喻户晓的自旋锁了,在竞争的情况下它会一直自旋,即忙等(busy wait),而不是阻塞。所以 spin lock 是 NBS 的一种。

NBS 的另一类比较高级的形式是 lock-free,从名字上也很容易看出,就是无锁。判断 lock-free 的一个标准是:如果正在进行操作的线程休眠了,别的线程依旧可以进来照常操作。这样就直接把 spin lock 给排除在外了。lock-free 通常使用的是 RMW (read-modify-write)原语,由硬件来支持,它最常见的一种实现是 CAS,即 Compare And Swap,在 x86 上对应的是 cmpxchg 指令。Linux 内核中把 cmpxchg 给封装成了一个通用的函数。第一次看到这个函数感觉有些别扭,估计是出于性能的考虑最新的内核代码中的实现还不如之前的实现容易让人读懂,下面是 2.6.32 中它 generic 的实现:

[c]
static inline unsigned long __cmpxchg(volatile unsigned long *m,
unsigned long old, unsigned long new)
{
unsigned long retval;
unsigned long flags;

    local_irq_save(flags);
    retval = *m;
    if (retval == old)
            *m = new;
    local_irq_restore(flags);
    return retval;

}
[/c]

比较新的 gcc 中有和它对应的一个内置原子函数 __sync_bool_compare_and_swap()。cmpxchg 一个典型的用例是 lockless list 的实现,见其插入操作的实现:

[c]
static inline bool llist_add(struct llist_node new, struct llist_head head)
{
struct llist_node entry, old_entry;

    entry = head->first;
    for (;;) {
            old_entry = entry;
            new->next = entry;
            entry = cmpxchg(&head->first, old_entry, new);
            if (entry == old_entry)
                    break;
    }

    return old_entry == NULL;

}
[/c]

我们可以看出,它虽然是 lock-free 的,但是避免不了等待(wait),因为这种 cmpxchg 一般都套在一个循环中直到操作成功才会退出,不成功就会一直尝试。不运行的话你根本不会知道它到底会尝试多少次才能成功。

另外一种更加高级的 NBS 就是 wait-free 了,这个可以说是大家梦寐以求的东西,它不阻塞,不用忙等,连等待都不用,也可以避免竞争!减少了很多 CPU 的浪费!显然 wait-free 一定是 lock-free 的,它们之间最大的区别是:等待的次数是否有上限。Wait-free 要求每个线程都可以在有限的步骤之内完成操作,不管其它线程如何!无奈这个太难实现,而且一般都比较复杂,即使实现了它的 overhead 也未必就比等价的 lock-free 的小。网上有很多相关的资料,你可以自行搜索一下。

参考资料:

1. http://julien.benoist.name/lockfree.html
2. http://audidude.com/?p=363
3. http://burtleburtle.net/bob/hash/lockfree.html
4. http://en.wikipedia.org/wiki/Non-blocking_synchronization
5. http://en.wikipedia.org/wiki/Lock-free_and_wait-free_algorithms