C ++ 11中的无锁缓存实现

时间:2016-03-26 19:28:00

标签: c++ multithreading c++11 caching lock-free

在C ++ 11中是否有任何方法可以为对象实现无锁缓存,从多个线程访问是安全的?我想要缓存的计算并不是非常便宜,但也不是非常昂贵,因此在我的情况下需要锁定会破坏缓存的目的。 IIUC,std::atomic并不保证无锁。

编辑:由于计算费用并不昂贵,我实际上并不介意它运行一次或两次太多。但我 - 需要确保所有消费者都能获得正确的价值。在下面的天真示例中,这是不能保证的,因为由于内存重新排序,线程可能会获得未初始化的m_val值,因为另一个线程集m_alreadyCalculated是的,但还没有设定m_val的价值。

Edit2:下面的评论指出,对于基本类型,std::atomic可能是无锁的。如果是,在下面的示例中使用C ++ 11的内存排序的正确方法是什么,以确保将m_alreadyCalculated设置为true是不可能的在m_val的值设置之前?

非线程安全缓存示例:

class C {
public:
   C(int param) : m_param(param) {}

   getValue() {
      if (!m_alreadyCalculated) {
          m_val = calculate(m_param);
          m_alreadyCalculated = true;
      }
      return m_val;
   }

   double calculate(int param) {
       // Some calculation
   }

private:
   int m_param;
   double m_val;
   bool m_alreadyCalculated = false;
}

3 个答案:

答案 0 :(得分:2)

考虑一下:

class C {
public:
   double getValue() {
      if (alreadyCalculated == true)
         return m_val;

      bool expected = false;
      if (calculationInProgress.compare_exchange_strong(expected, true)) {
         m_val = calculate(m_param);
         alreadyCalculated = true;
      // calculationInProgress = false;
      }
      else {
     //  while (calculationInProgress == true)
         while (alreadyCalculated == false)
            ; // spin
      }
      return m_val;
   }

private:
   double m_val;
   std::atomic<bool> alreadyCalculated {false};
   std::atomic<bool> calculationInProgress {false};
};

事实上它并不是无锁的,里面有旋转锁。但是,如果您不想通过多个线程运行calculate(),我认为您无法避免这种锁定。

getValue()在这里变得更加复杂,但重要的是,计算m_val后,它将始终在第一个if语句中立即返回。

<强>更新

出于性能原因,将整个类填充到缓存行大小也是一个好主意。

更新2

原始答案中有一个错误,感谢JVApen指出这一点(标记为注释)。变量calculationInProgress最好重命名为calculationHasStarted

另请注意,此解决方案假定calculate()不会引发异常。

答案 1 :(得分:0)

std :: atomic不保证是无锁的,但您可以查看std::atomic<T>::is_lock_free()std::atomic::is_always_lock_free()以查看您的实现是否可以执行此锁定。

另一种方法可能是使用std::call_once,但是根据我的理解,这更糟糕,因为它意味着阻止其他线程。

因此,在这种情况下,我会使用std :: atomic来同时使用m_val和alreadyCalculated。其中包含2个(或更多)线程计算相同结果的风险。

答案 2 :(得分:0)

这里只回答一个技术问题:要确保在标志之前更新值,请使用发布语义更新标志。释放语义的含义是这个更新必须(被视为)在所有先前的更新之后发生。在x86上,它只表示更新前的编译器屏障,并对内存进行更新,而不是注册,如下所示:

asm volatile("":::"memory");
*(volatile bool*)&m_alreadyCalculated = true;

这正是原子集在发布语义中所做的事情