C++ 是否可以为单个编写器发布/检查更新类+;读者使用记忆、放松或获取/释放来提高效率? 介绍

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,但

我有一个小类,它使用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:最容易使用,因为它就像我们对变量使用的自然写入一样,可以立即刷新其他线程加载函数的值

洛克弗里克斯级
模板
无锁类
{
公众:
无效推送(常量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.
    }