繁体   English   中英

使用std :: atomic而不使用互斥锁的模数的C ++线程安全增量

[英]C++ thread-safe increment with modulo without mutex using std::atomic

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

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;
};

选择后可以安全地使用Modulo

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 ];
    }

您可以将mNextBufferIndex声明为std::atomic_ullong然后使用

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

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

使用非常大的无符号将避免计数器包装时出现的问题。

据我所知,这有硬件辅助联锁操作。 一个这样的操作是增量。 您不需要使其更复杂,因为模运算可以独立于增量运算。

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关于增量问题纸质幻灯片

当您有两个输入时,它是不可能的。 你可以用原子做的就是使用CPU支持的原子指令(我还没有听到可以做增量加模数作为操作的芯片),或者你可以先做计算而不是设置输入所提供的值不要改变 - 但你的功能确实有两个输入,所以这也不起作用。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM