使用双buffer无锁化

4,567 阅读2分钟


使用锁


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

// write thread
{
  LockGuard guard(lock);
  obj.load();  // load会对obj的属性进行重写
}

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

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


缩小临界区


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

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对象放到一个数组中,用一个原子的下标表示当前的读对象,此时“原子交换”,只需要原子赋值下标即可。

伪代码如下:

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排雷
读写锁的性能一定更好吗
面向数据编程

转载请注明出处: blog.guoyb.com/2018/03/17/…

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