简体   繁体   English

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

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

I need a thread-safe pool of Buffer-objects used in a round-robin fashion. 我需要一个以循环方式使用的线程安全的Buffer-objects池。 I normally would just put a mutex in there to make the increment and modulo thread safe, but is it possible to write it using std::atomic? 我通常会在那里放一个互斥量来使增量和模数线程安全,但是可以用std :: atomic写它吗? Here's a sample interface. 这是一个示例界面。 If it makes things easier the total number of buffers can be a power of two. 如果它使事情变得更容易,缓冲区的总数可以是2的幂。 The next buffer index is never accessed outside of the class. 永远不会在类之外访问下一个缓冲区索引。

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 can be safely used after choosing 选择后可以安全地使用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 ];
    }

You could just declare the mNextBufferIndex an std::atomic_ullong and then use 您可以将mNextBufferIndex声明为std::atomic_ullong然后使用

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

The increment will be atomic and you compute the modulo just before returning. 增量将是原子的,您在返回之前计算模数。

Using a very large unsigned will avoid problems that would occur when the counter wraps. 使用非常大的无符号将避免计数器包装时出现的问题。

To my knowledge, there's hardware assisted interlocked operations for this. 据我所知,这有硬件辅助联锁操作。 One such operation is increment. 一个这样的操作是增量。 You don't need to make it more complicated since the modulo operation can act independently from the increment operation. 您不需要使其更复杂,因为模运算可以独立于增量运算。

std::atomic does overload the operator++ and I would think that it has the atomic guarantee. 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];
}

If you wanted to do it with the modulo or any other complex arithmetic you use a compare and exchange version. 如果您想使用模数或任何其他复杂算法,则使用比较和交换版本。

The idea between compare and exchange or compare and swap is that you do your computation and when you want to write the value back to the memory location (shared or otherwise) it will only succeed if no one else did not modify the value in the meantime (if they do you just retry the operation, busy-wait). 比较和交换或比较和交换之间的想法是你进行计算,当你想将值写回内存位置(共享或其他)时,只有在此期间没有其他人没有修改值时它才会成功(如果他们你只是重试操作,忙等待)。 This requires only a predictable numbering scheme which is often very possible to do. 这仅需要可预测的编号方案,这通常是非常可能的。

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

Assuming mNextBufferIndex is an std::atomic . 假设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];
}

You may think that this is more akin to optimistic concurrency control but if you limit your self to a definition of what is atomic that is too narrow you won't get anything done. 您可能认为这更类似于乐观并发控制,但如果您将自己限制为对原子的定义过于狭窄,那么您将无法完成任何操作。

Putting aside that what I'm computing here is complete nonsense is shows how powerful the the compare_exchange operation is and how to use it to make any arithmetic atomic. 暂且不谈我在这里计算的是完全无意义的,这表明compare_exchange操作有多强大,以及如何使用它来制作任何算术原子。 The problem is really when you have multiple computations that depend on each other. 当您有多个相互依赖的计算时,问题就出现了。 You need to code in a lot of recovery routines when this is the case. 在这种情况下,您需要在许多恢复例程中进行编码。

Though, interlocked operations by themselves are not free and evict cache lines in the processor. 但是,互锁操作本身并不是免费的,并且会驱逐处理器中的高速缓存行。

For reference I can recommend Mike Acton's paper slides about the increment problem . 作为参考,我可以推荐Mike Acton关于增量问题纸质幻灯片

It is not possible when you have two inputs. 当您有两个输入时,它是不可能的。 All you can do with atomic is either use CPU-supported atomic instructions (and I am yet to hear of the chip which can do increment plus modulo as an operation), or you can do calculation first and than set the value provided the input didn't change - but your function does have two inputs, so this will not work either. 你可以用原子做的就是使用CPU支持的原子指令(我还没有听到可以做增量加模数作为操作的芯片),或者你可以先做计算而不是设置输入所提供的值不要改变 - 但你的功能确实有两个输入,所以这也不起作用。

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

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