无锁并发编程简谈

(有半年没有写博客了,时间飞逝呀。。。。 :o )

这里简单介绍一下无锁并发编程。

现在CPU的核越来越多,多线程、并发编程已经成为趋势。一涉及并发,同步是绕不开的话题。一般的方法是使用Mutex、旋转锁、条件变量等系统提供的方法来进行同步。(一个问题,Mutex和自旋锁的区别?)如果并发不太大,qps是数百时,这些方法还没有问题,但是当qps增加到数千时,这些同步方法的开销就太大了,会对系统产生不小的开销,影响程序的性能。怎么办?

一种方法是先创建一堆线程池,然后将query计算后(比如取模),传给相应的线程处理,这样即使使用传统的同步方法,还是可以降低同步的开销。然而对于全局唯一的资源,还是不能降低同步的开销。这样,就引出了无锁并发的概念。

无锁并发(Lock-free),就是不使用锁来进行同步,那就要知道CAS(compare&set或者compare&swap)的概念。其实CAS很简单,就是取出资源或者版本号后,再进行操作前,再进行比较,看期间资源或版本号有没有改变,如果没有,则表明没有其他程序修改过,可以直接修改。如果当前的数据和之前的数据不一样,则表明期间资源被修改过,则之前的资源地址后者版本号已经失效,需要重新获取,之后再次比较和操作。

要进行CAS,要了解[color=darkred]gcc内置[/color]提供的一系列原子操作的函数:

type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)

type __sync_add_and_fetch (type *ptr, type value, ...)
type __sync_sub_and_fetch (type *ptr, type value, ...)
type __sync_or_and_fetch (type *ptr, type value, ...)
type __sync_and_and_fetch (type *ptr, type value, ...)
type __sync_xor_and_fetch (type *ptr, type value, ...)
type __sync_nand_and_fetch (type *ptr, type value, ...)
这两组函数的区别在于第一组返回更新前的值,第二组返回更新后的值。


type可以是1,2,4或8字节长度的int类型,即:
int8_t / uint8_t
int16_t / uint16_t
int32_t / uint32_t
int64_t / uint64_t
后面的可扩展参数(...)用来指出哪些变量需要memory barrier,因为目前gcc实现的是full barrier(类似于linux kernel 中的mb(),表示这个操作之前的所有内存操作不会被重排序到这个操作之后),所以可以略掉这个参数。
bool __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
这两个函数提供原子的比较和交换,如果*ptr == oldval,就将newval写入*ptr,
第一个函数在相等并写入的情况下返回true.
第二个函数在返回操作之前的值。
__sync_synchronize (...)
发出一个full barrier.

这样,就可以利用这些内置的函数,开发一个无锁的stack:

template
class Stack {
typedef struct Node {
T data;
Node* next;
Node(const T& d) : data(d), next(0) { }
} Node;
Node *top;
public:
Stack( ) : top(0) { }
void push(const T& data);
T pop( ) throw (…);
};

void Stack::push(const T& data)
{
Node *n = new Node(data);
while (1) {
n->next = top;
if (__sync_bool_compare_and_swap(&top, n->next, n)) { // CAS
break;
}
}
}

T Stack::pop( )
{
while (1) {
Node* result = top;
if (result == NULL)
throw std::string(“Cannot pop from empty stack”);
if (top && __sync_bool_compare_and_swap(&top, result, result->next)) { // CAS
return result->data;
}
}
}

嗯,基本就是这么简单,当然还有一些复杂的场景需要探索,要在使用中摸索。

其实想一想,CAS最终还是用到了“锁”,不过这个锁是编程语言控制的最小的同步锁,所以颗粒是最小的,对系统的开销也是最小的。

后记:无锁编程中一个很大的问题是ABA问题(变量中间被修改后又复原了),可能造成程序的隐患,所以无锁编程局限性还是很大的。

参考:
[url]http://blog.163.com/xychenbaihu@yeah/blog/static/1322296552013373236132/[/url]

[url]https://blog.csdn.net/youfuchen/article/details/23179799[/url]

你可能感兴趣的:(无锁并发编程简谈)