微信号:TechTalking

介绍:这可能是最接地气的技术分享公众号,我愿在这里与你们分享成长路上的点点滴滴. 关注各类Linux相关后端技术,关爱程序员的生活,与你分享我的思考. 技术·生活·思考

使用双buffer无锁化

2018-03-17 11:45 茄仁波切

点击上方蓝色文字“后端技术小黑屋”,关注茄仁波切的公众号吧~


使用锁

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

// 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排雷

读写锁的性能一定更好吗

面向数据编程


题图:xjqwangj

授权:CC0协议



 
后端技术小黑屋 更多文章 protobuf中set_allocated_xxx排雷 开工大吉 跳出思维定式 读写锁的性能一定更好吗 狗年快乐
猜您喜欢 今日好书丨《人脸识别原理与实战:以MATLAB为工具》 KVM虚拟化网络优化技术总结 Vim自动补全 RAID卡未来之路:除了NVMe还有啥? 谷歌工程师怼上Yann LeCun:你对Google Brain的评价完全是错的