2015-11-05 40 views
2

模块无需互斥我需要一个线程安全的缓冲区对象以循环方式使用。我通常会在那里放置一个互斥体,以使增量和模数线程安全,但可以使用std :: atomic写入它吗?这里有一个示例界面。如果它使事情更容易,缓冲区的总数可以是2的幂。下一个缓冲区索引永远不会在类之外访问。C++线程安全增量模块使用std :: atomic

class Buffer; 

class BufferManager 
{ 
public: 

    BufferManager(size_t totalBuffers = 8) : mNextBufferIndex(0), mTotalBuffers(totalBuffers) 
    { 
     mBuffers = new Buffer*[mTotalBuffers]; 
    } 

    Buffer* GetNextBuffer() 
    { 
     // How to make this operation atomic? 
     size_t index = mNextBufferIndex; 

     mNextBufferIndex = (mNextBufferIndex + 1) % mTotalBuffers; 

     return mBuffers[index]; 
    } 

private: 
    Buffer**   mBuffers; 
    size_t    mNextBufferIndex; 
    size_t    mTotalBuffers; 
}; 

回答

3

模可以选择

std::atomic<size_t> mNextBufferIndex; 

    Buffer* GetNextBuffer() 
    { 
     // How to make this operation atomic? 
     size_t index = mNextBufferIndex ++; 

     size_t id = index % mTotalBuffers; 

     // If size could wrap, then re-write the modulo value. 
     // oldValue keeps getting re-read. 
     // modulo occurs when nothing else updates it. 
     size_t oldValue =mNextBufferIndex; 
     size_t newValue = oldValue % mTotalBuffers; 
     while (!m_mNextBufferIndex.compare_exchange_weak(oldValue, newValue, std::memory_order_relaxed)) 
      newValue = oldValue % mTotalBuffers; 
     return mBuffers[id ]; 
    } 
+0

啊,这是有道理的 - 下一个缓冲区索引实际上并不需要*需要模数,只有在线程安全的情况下才会增加。只要mNextBufferIndex的类型的最大值大于或等于mTotalBuffers(就像这里),这就好了。谢谢。 – Steven

+0

OP可能没问题,但它与原子操作或互斥体内的整个操作完全不同。 – SergeyA

+0

@SergeyA我不清楚,你是对的 - 我认为这是正确的答案。 – Steven

-1

当您有两个输入时,这是不可能的。你可以用原子做的所有事情都是使用CPU支持的原子指令(并且我还没有听说可以做增量加模作为操作的芯片),或者你可以首先进行计算,然后设置输入提供的值不会改变 - 但你的功能确实有两个输入,所以这也不起作用。

1

你可以只声明mNextBufferIndexstd::atomic_ullong,然后用

return mBuffers[(mNextBufferIndex++) % mTotalBuffers]; 

的增量将是原子,你只是在返回之前计算模。

使用非常大的未签名将避免计数器换行时会发生的问题。

+0

当然,我可以使用无符号的,但是我无法控制范围。当它工作时它是一个很好的解决方案,但是在这里我需要用户定义的缓冲区数量。 – Steven

+0

@Steven:在使用索引之前计算模数! – 6502

+0

对 - 你和mksteve都给了我同样的东西 - 我现在看到了。 – Steven

0

据我所知,之后可以安全使用,有硬件辅助联锁操作这一点。一个这样的操作是增量。由于模操作可以独立于增量操作,因此不需要使其更复杂。

std::atomic确实超载operator++,我会认为它有原子保证。

Buffer* GetNextBuffer() 
{ 
    // Once this inc operation has run the return value 
    // is unique and local to this thread the modulo operation 
    // does not factor into the consistency model 
    auto n = mNextBufferIndex++; 
    auto pool_index = n % mTotalBuffers; 
    return mBuffers[pool_index]; 
} 

如果您想使用模或任何其他复杂的算术,使用比较和交换版本。

比较和交换或比较和交换之间的想法是,你做你的计算,当你想将值写回内存位置(共享或其他),它只会成功,如果没有其他人不修改在此期间的价值(如果他们只是重试操作,则等待)。这只需要一个可预测的编号方案,这通常是很有可能的。

Buffer* GetNextBuffer() 
{ 
    // Let's assume that we wanted to do this 
    auto n = (mNextBufferIndex % mTotalBuffers) ++; 
    mNextBufferIndex = n; 
    return mBuffers[n]; 
} 

假设mNextBufferIndexstd::atomic

Buffer* GetNextBuffer() 
{ 
    // Let's assume that we wanted to do this 
    auto n = (mNextBufferIndex % mTotalBuffers)++; 
    // This will now either succeed or not in the presence of concurrency 
    while (!std::compare_exchange_weak(mNextBufferIndex, n)) { 
    n = (mNextBufferIndex % mTotalBuffers)++; 
    } 
    return mBuffers[n]; 
} 

你可能认为这更像是乐观并发控制,但如果你限制你自己对什么是原子太窄,你不会得到任何东西做了定义。

撇开我在这里计算的是完全的废话是表明compare_exchange操作是多么强大,以及如何使用它来使任何算术原子。问题是当你有多个相互依赖的计算时。在这种情况下,您需要在很多恢复例程中编写代码。

虽然,互锁操作本身并不是免费的,并且驱逐处理器中的缓存线。

仅供参考,我可以推荐Mike Acton的paper slides增量问题

+0

不同意。这个操作有两个变量输入,所以它不能成为真正的原子(所以它与互斥体没有区别)。 – SergeyA

+0

@SergeyA取决于你的原子意味着什么。对我而言,最终的结果是整个操作发生或者不发生。这个操作的全部目的只是为了保持一致性。我会用一个例子更新我的答案。 –

+0

我的意思是,它考虑到两个操作数。在你的情况下,你首先计算n作为mNextBufferIndex和mTotalBuffers的乘积。然后,您只使用其中的一个 - mNextBufferIndex - 作为CaS的参数。如果第二个过程发生变化,您的CaS将不会检测到它,并且其值会有所不同。基本上,你将最终使用一个变量的新值和另一个变量的旧值。在语意上,它与锁定不同,这将保证两个值都是旧的。 – SergeyA

相关问题