使用双buffer无锁化



使用锁


面对多线程读写同一块内存的情况,书接上文读写锁的性能一定更好吗,假设我们已经选定了一种锁,那么最直接想到的做法是一般这样的:

1
2
3
4
5
6
7
8
9
10
11
// write thread
{
LockGuard guard(lock);
obj.load(); // load会对obj的属性进行重写
}

// read thread
{
LockGuard guard(lock);
useObj(obj); // useObj会读取obj的属性
}

但是这样的话,会把obj的读写全部放在锁中,临界区太大,对并发性有较大影响。


缩小临界区


为了缩小临界区,我们往往会牺牲一点内存,空间换时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
shared_ptr<Obj> obj;

// write thread
{
shared_ptr<Obj> tmp = std::make_shared<Obj>();
tmp.load();
{
LockGuard guard(lock);
obj = tmp;
}
}

// read thread
{
shared_ptr<Obj> tmp = std::make_shared<Obj>();
{
LockGuard guard(lock);
tmp = obj;
}
useObj(tmp);
}

现在,我们已经将Obj对象的load和useObj全部移除了临界区,也就意味着,这一部分的运算,可以实现并发。

其实,我们还可以使用双buffer技术,来彻底无锁化。


双buffer


所谓双buffer技术,其实就是准备两个Obj,一个用来读,一个用来写。写完成之后,原子交换两个Obj;之后的读操作,都放在交换后的读对象上,而原来的读对象,在原有的“读操作”完成之后,又可以进行写操作了。

但是,这里有两个问题:

1.“原子交换”如何做?  
2.如何判断,原来的读对象上的读取操作都结束了?

先看第二个问题,可以通过shared_ptr的use_count()获得其引用计数,来判断当前是否还有其他线程在读取这个Obj;

但是,shared_ptr的读写无法做到原子操作——shared_ptr的引用计数是原子的,但是shared_ptr本身不是。

这时,可以换个思路。我们将两个shared_ptr对象放到一个数组中,用一个原子的下标表示当前的读对象,此时“原子交换”,只需要原子赋值下标即可。

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
std::vector<shared_ptr<Obj>> obj_buffers;
std::atomic_size_t curr_idx;

// write thread
{
size_t prepare = 1 - curr_idx.load();
if (obj_buffers[prepare].use_count() > 1) {
continue;
}
obj_buffers[prepare]->load();
curr_idx = prepare;
}

// read thread
{
shared_ptr<Obj> tmp = obj_buffers[curr_idx.load()];
useObj(tmp);
}

这里需要注意的是,C++的基本类型并不保证原子性,所以这里需要使用C++11中新增的std::atomic原子类型作为下标。



推荐阅读:
protobuf中set_allocated_xxx排雷
读写锁的性能一定更好吗
面向数据编程

转载请注明出处: http://blog.guoyb.com/2018/03/17/double-buffer/

欢迎使用微信扫描下方二维码,关注我的微信公众号TechTalking,技术·生活·思考:
后端技术小黑屋

Comments