我一直在努力对原子在C ++中的工作方式有一个根本性的误解。我已经编写了以下代码,以使用原子变量作为索引来实现快速环形缓冲区,以便多个线程可以读写该缓冲区。我将代码缩减为这种简单的情况(我意识到仍然有些长。抱歉。)。如果我在Linux或Mac OS X上运行此程序,它有时会起作用,但也至少有10%的时间会引发异常。它似乎也运行得非常快,然后放慢速度,甚至可能再次加快速度,这也表明有些不正确。我无法理解我的逻辑缺陷。我需要围栏吗?
这是它要做什么的简单描述: 使用compare_exchange_weak方法可以提高原子索引变量的质量。这是为了确保可以独占访问从其移出索引的插槽。实际上需要两个索引,因此当我们环绕环形缓冲区时,值不会被覆盖。注释中嵌入了更多详细信息。
#include <mutex>
#include <atomic>
#include <iostream>
#include <cstdint>
#include <vector>
#include <thread>
using namespace std;
const uint64_t Nevents = 1000000;
std::atomic<uint64_t> Nwritten(0);
std::atomic<uint64_t> Nread(0);
#define MAX_EVENTS 10
mutex MTX;
std::atomic<uint32_t> iread{0}; // The slot that the next thread will try to read from
std::atomic<uint32_t> iwrite{0}; // The slot that the next thread will try to write to
std::atomic<uint32_t> ibegin{0}; // The slot indicating the beginning of the read region
std::atomic<uint32_t> iend{0}; // The slot indicating one-past-the-end of the read region
std::atomic<uint64_t> EVENT_QUEUE[MAX_EVENTS];
//-------------------------------
// WriteThreadATOMIC
//-------------------------------
void WriteThreadATOMIC(void)
{
MTX.lock();
MTX.unlock();
while( Nwritten < Nevents ){
// Copy (atomic) iwrite index to local variable and calculate index
// of next slot after it
uint32_t idx = iwrite;
uint32_t inext = (idx + 1) % MAX_EVENTS;
if(inext == ibegin){
// Queue is full
continue;
}
// At this point it looks like slot "idx" is available to write to.
// The next call ensures only one thread actually does write to it
// since the compare_exchange_weak will succeed for only one.
if(iwrite.compare_exchange_weak(idx, inext))
{
// OK, we've claimed exclusive access to the slot. We've also
// bumped the iwrite index so another writer thread can try
// writing to the next slot. Now we write to the slot.
if(EVENT_QUEUE[idx] != 0) {lock_guard<mutex> lck(MTX); cerr<<__FILE__<<":"<<__LINE__<<endl; throw -1;} // Dummy check. This should NEVER happen!
EVENT_QUEUE[idx] = 1;
Nwritten++;
if(Nread>Nwritten) {lock_guard<mutex> lck(MTX); cerr<<__FILE__<<":"<<__LINE__<<endl; throw -3;} // Dummy check. This should NEVER happen!
// The idx slot now contains valid data so bump the iend index to
// let reader threads know. Note: if multiple writer threads are
// in play, this may spin waiting for another to bump iend to us
// before we can bump it to the next slot.
uint32_t save_idx = idx;
while(!iend.compare_exchange_weak(idx, inext)) idx = save_idx;
}
}
lock_guard<mutex> lck(MTX);
cout << "WriteThreadATOMIC done" << endl;
}
//-------------------------------
// ReadThreadATOMIC
//-------------------------------
void ReadThreadATOMIC(void)
{
MTX.lock();
MTX.unlock();
while( Nread < Nevents ){
uint32_t idx = iread;
if(idx == iend) {
// Queue is empty
continue;
}
uint32_t inext = (idx + 1) % MAX_EVENTS;
// At this point it looks like slot "idx" is available to read from.
// The next call ensures only one thread actually does read from it
// since the compare_exchange_weak will succeed for only one.
if( iread.compare_exchange_weak(idx, inext) )
{
// Similar to above, we now have exclusive access to this slot
// for reading.
if(EVENT_QUEUE[idx] != 1) {lock_guard<mutex> lck(MTX); cerr<<__FILE__<<":"<<__LINE__<<endl; throw -2;} // Dummy check. This should NEVER happen!
EVENT_QUEUE[idx] = 0;
Nread++;
if(Nread>Nwritten) {lock_guard<mutex> lck(MTX); cerr<<__FILE__<<":"<<__LINE__<<endl; throw -4;} // Dummy check. This should NEVER happen!
// Bump ibegin freeing idx up for writing
uint32_t save_idx = idx;
while(!ibegin.compare_exchange_weak(idx, inext)) idx = save_idx;
}
}
lock_guard<mutex> lck(MTX);
cout << "ReadThreadATOMIC done" << endl;
}
//-------------------------------
// main
//-------------------------------
int main(int narg, char *argv[])
{
int Nwrite_threads = 4;
int Nread_threads = 4;
for(int i=0; i<MAX_EVENTS; i++) EVENT_QUEUE[i] = 0;
MTX.lock(); // Hold off threads until all are created
// Launch writer and reader threads
vector<std::thread *> atomic_threads;
for(int i=0; i<Nwrite_threads; i++){
atomic_threads.push_back( new std::thread(WriteThreadATOMIC) );
}
for(int i=0; i<Nread_threads; i++){
atomic_threads.push_back( new std::thread(ReadThreadATOMIC) );
}
// Release all threads and wait for them to finish
MTX.unlock();
while( Nread < Nevents) {
std::this_thread::sleep_for(std::chrono::microseconds(1000000));
cout << "Nwritten: " << Nwritten << " Nread: " << Nread << endl;
}
// Join threads
for(auto t : atomic_threads) t->join();
}
当我在调试器中发现此错误时,通常是由于EVENT_QUEUE插槽中的值错误。有时,尽管Nread的数量超过了Nwrite的数量,但这似乎是不可能的。我认为我不需要栅栏,因为一切都是原子的,但是由于我不得不质疑我认为我知道的一切,所以我现在不能说。
任何建议或见识将不胜感激。
答案 0 :(得分:1)
我之前已经建立了这种确切的结构,您的实现几乎是我曾经遇到过的问题。问题归结为以下事实:环形缓冲区由于不断重复使用同一内存,因此特别容易受到ABA问题的影响。
如果您不知道ABA problem是获取值A
的地方,则稍后请检查该值是否仍为A
以确保您的状态良好状态,但您不知道,该值实际上从A
更改为B
,然后又更改为A
。
我会指出您的作者中的一个情况,但读者有同样的问题:
// Here you check if you can even do the write, lets say it succeeds.
uint32_t idx = iwrite;
uint32_t inext = (idx + 1) % MAX_EVENTS;
if(inext == ibegin)
continue;
// Here you do a compare exchange to ensure that nothing has changed
// out from under you, but lets say your thread gets unscheduled, giving
// time for plenty of other reads and writes occur, enough writes that
// your buffer wraps around such that iwrite is back to where it was at.
// The compare exchange can succeed, but your condition above may not
// still be good anymore!
if(iwrite.compare_exchange_weak(idx, inext))
{
...
我不知道是否有更好的方法来解决此问题,但我认为在交换后添加额外的支票仍然有问题。我最终通过添加其他原子来跟踪写保留计数和读保留计数来解决该问题,以便即使缠绕了它们,我也可以保证该空间仍然可以使用。可能还有其他解决方案。
免责声明:这可能不是您唯一的问题。