C++ 是否可以为单个编写器发布/检查更新类+;读者使用记忆、放松或获取/释放来提高效率? 介绍
我有一个小类,它使用std::atomic实现无锁操作。由于这个类被大量调用,它影响了性能,我遇到了麻烦 类描述 该类类似于后进先出,但一旦调用了pop()函数,它只返回其环形缓冲区中最后写入的元素(仅当自上次pop()以来有新元素时) 一个线程正在调用push(),另一个线程正在调用pop() 我读过的资料来源 由于这占用了我太多的计算机时间,我决定进一步研究std::atomic类及其内存顺序。我在StackOverflow和其他来源和书籍中阅读了大量的memory_order post,但我无法清楚地了解不同的模式。特别是,我在获取和释放模式之间苦苦挣扎:我也不明白为什么它们与内存顺序cst不同 根据我自己的研究,我用我的话思考每个记忆顺序 内存\u顺序\u松弛:在同一线程中,原子操作是即时的,但其他线程可能无法立即看到最新的值,它们需要一些时间才能更新。代码可以由编译器或操作系统自由重新排序 内存\u顺序\u获取/释放:由atomic::load使用。它防止在此之前的代码行被重新排序(编译器/操作系统可以在这一行之后重新排序),并在该线程或另一个线程中使用内存顺序释放或内存顺序cst读取存储在此原子上的最新值内存\顺序\释放也可以防止代码在重新排序后被重新排序。因此,在acquire/release中,两者之间的所有代码都可以被操作系统洗牌。我不确定这是在同一个线程之间,还是在不同的线程之间 memory\u order\u seq\u cst:最容易使用,因为它就像我们对变量使用的自然写入一样,可以立即刷新其他线程加载函数的值 洛克弗里克斯级C++ 是否可以为单个编写器发布/检查更新类+;读者使用记忆、放松或获取/释放来提高效率? 介绍,c++,atomic,lock-free,memory-barriers,stdatomic,C++,Atomic,Lock Free,Memory Barriers,Stdatomic,我有一个小类,它使用std::atomic实现无锁操作。由于这个类被大量调用,它影响了性能,我遇到了麻烦 类描述 该类类似于后进先出,但一旦调用了pop()函数,它只返回其环形缓冲区中最后写入的元素(仅当自上次pop()以来有新元素时) 一个线程正在调用push(),另一个线程正在调用pop() 我读过的资料来源 由于这占用了我太多的计算机时间,我决定进一步研究std::atomic类及其内存顺序。我在StackOverflow和其他来源和书籍中阅读了大量的memory_order post,但
模板
无锁类
{
公众:
无效推送(常量T和元素)
{
const int wPos=m_position.load(std::memory_order_seq_cst);
const int nextPos=getNextPos(wPos);
m_buffer[nextPos]=元素;
m_position.store(下一个操作系统,标准::内存顺序顺序cst);
}
康斯特布尔波普(T&returnedElement)
{
const int wPos=m_position.exchange(-1,std::memory_order_seq_cst);
如果(WPO!=-1)
{
returnedElement=m_缓冲区[wPos];
返回true;
}
其他的
{
返回false;
}
}
私人:
静态constexpr int maxElements=8;
静态constexpr int getNextPos(int pos)noexcept{return(++pos==maxElements)?0:pos;}
std::数组m_缓冲区;
std::原子m_位置{-1};
};
我多么希望它能得到改进
因此,我的第一个想法是在所有原子操作中使用内存顺序松弛,因为pop()线程处于循环中,每10-15毫秒在pop函数中寻找可用的更新,然后允许它在第一个pop()函数中失败,以便稍后意识到有一个新的更新。这只是几毫秒
另一个选择是使用release/acquire,但我不确定。在allstore()中使用release,在allload()函数中使用acquire
不幸的是,我描述的所有内存顺序似乎都能正常工作,如果它们应该失败,我不确定它们什么时候会失败
最终的
请问,你能告诉我,如果你看到一些问题,使用放松记忆顺序在这里?或者我应该使用release/acquire(也许对这些有进一步的解释可以帮助我)?为什么?
我认为released对于这个类来说是最好的,在它的所有store()或load()中。但我不确定
谢谢你的阅读
编辑:额外解释:
因为我看到每个人都要求“char”,所以我把它改为int,问题解决了!但这不是我想要解决的问题
正如我前面所说的,这个类很可能是后进先出的,但如果有,它只影响最后一个元素
我有一个大结构T(可复制和可复制),我必须以无锁的方式在两个线程之间共享。所以,我知道的唯一方法是使用一个循环缓冲区,它为T写入最后一个已知的值,以及一个原子,它知道最后一个写入的值的索引。如果没有,索引将为-1
请注意,我的推线程必须知道何时有“new T”可用,这就是pop()返回bool的原因
再次感谢所有试图帮助我处理内存订单的人!:)
阅读解决方案后:
模板
无锁类
{
公众:
LockFreeEx(){}
LockFreeEx(const T&initValue):m_数据(initValue){}
//写入线程-可以很慢,每500-800毫秒调用一次
无效发布(常量和元素)
{
//我使用acquire而不是relaxed来确保wPos始终是最新的w_writePos值,并且nextPos计算出正确的值
const int wPos=m_writePos.load(std::memory_order_acquire);
const int nextPos=(wPos+1)%bufferMaxSize;
m_buffer[nextPos]=元素;
m_writePos.store(nextPos,std::memory_order_release);
}
//读取线程-需要非常快-在循环开始时每2ms调用一次
内联void更新()
{
//我应该改为relaxed吗?我不获取新值或旧值并不重要,因为我将很快再次调用此函数,一次又一次,一次又一次。。。
常量int writeIndex=m_writePos.load(std::memory_order_acquire);
//仅在出现新内容时更新…T可能是一个重结构
如果(m_readPos!=写入索引)
{
template<typename T>
class LockFreeEx
{
public:
LockFreeEx() {}
LockFreeEx(const T& initValue): m_data(initValue) {}
// WRITE THREAD - CAN BE SLOW, WILL BE CALLED EACH 500-800ms
void publish(const T& element)
{
// I used acquire instead relaxed to makesure wPos is always the lastest w_writePos value, and nextPos calculates the right one
const int wPos = m_writePos.load(std::memory_order_acquire);
const int nextPos = (wPos + 1) % bufferMaxSize;
m_buffer[nextPos] = element;
m_writePos.store(nextPos, std::memory_order_release);
}
// READ THREAD - NEED TO BE VERY FAST - CALLED ONCE AT THE BEGGINING OF THE LOOP each 2ms
inline void update()
{
// should I change to relaxed? It doesn't matter I don't get the new value or the old one, since I will call this function again very soon, and again, and again...
const int writeIndex = m_writePos.load(std::memory_order_acquire);
// Updating only in case there is something new... T may be a heavy struct
if (m_readPos != writeIndex)
{
m_readPos = writeIndex;
m_data = m_buffer[m_readPos];
}
}
// NEED TO BE LIGHTNING FAST, CALLED MULTIPLE TIMES IN THE READ THREAD
inline const T& get() const noexcept {return m_data;}
private:
// Buffer
static constexpr int bufferMaxSize = 4;
std::array<T, bufferMaxSize> m_buffer;
std::atomic<int> m_writePos {0};
int m_readPos = 0;
// Data
T m_data;
};
m_buffer[nextPos] = element;
m_position.store(nextPos, std::memory_relaxed);
m_position.store(nextPos, std::memory_relaxed);
m_buffer[nextPos] = element;
const char wPos = m_position.load(std::memory_order_relaxed);
...
m_position.store(nextPos, std::memory_order_release);
...
const char wPos = m_position.exchange(-1, memory_order_acquire);
// Possible failure mode: writer wraps around between reads, leaving same m_position
// single-reader
const bool read(T &elem)
{
// FIXME: big hack to get this in a separate cache line from the instance vars
// maybe instead use alignas(64) int m_lastread as a class member, and/or on the other side of m_buffer from m_position.
static int lastread = -1;
int wPos = m_position.load(std::memory_order_acquire); // or cheat with relaxed to get asm that's like "consume"
if (lastread == wPos)
return false;
elem = m_buffer[wPos];
lastread = wPos;
return true;
}
template<typename T>
class WaitFreePublish
{
private:
struct {
alignas(32) T elem; // at most 2 elements per cache line
std::atomic<int8_t> claimed; // writers sets this to 0, readers try to CAS it to 1
// could be bool if we don't end up needing 3 states for anything.
// set to "1" in the constructor? or invert and call it "unclaimed"
} m_buffer[maxElements];
std::atomic<int> m_position {-1};
}
/// claimed flag per array element supports concurrent readers
// thread-safety: single-writer only
// update claimed flag first, then element, then m_position.
void publish(const T& elem)
{
const int wPos = m_position.load(std::memory_order_relaxed);
const int nextPos = getNextPos(wPos);
m_buffer[nextPos].claimed.store(0, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release); // make sure that `0` is visible *before* the non-atomic element modification
m_buffer[nextPos].elem = elem;
m_position.store(nextPos, std::memory_order_release);
}
// thread-safety: multiple readers are ok. First one to claim an entry gets it
// check claimed flag before/after to detect overwrite, like a SeqLock
const bool read(T &elem)
{
int rPos = m_position.load(std::memory_order_acquire);
int8_t claimed = m_buffer[rPos].claimed.load(std::memory_order_relaxed);
if (claimed != 0)
return false; // read-only early-out
claimed = 0;
if (!m_buffer[rPos].claimed.compare_exchange_strong(
claimed, 1, std::memory_order_acquire, std::memory_order_relaxed))
return false; // strong CAS failed: another thread claimed it
elem = m_buffer[rPos].elem;
// final check that the writer didn't step on this buffer during read, like a SeqLock
std::atomic_thread_fence(std::memory_order_acquire); // LoadLoad barrier
// We expect it to still be claimed=1 like we set with CAS
// Otherwise we raced with a writer and elem may be torn.
// optionally retry once or twice in this case because we know there's a new value waiting to be read.
return m_buffer[rPos].claimed.load(std::memory_order_relaxed) == 1;
// Note that elem can be updated even if we return false, if there was tearing. Use a temporary if that's not ok.
}