单个编写者+读者的发布/检查更新类可以使用memory_order_relaxed还是获取/发布以提高效率?

Jua*_*nto 3 c++ atomic lock-free memory-barriers stdatomic

介绍

我有一个小类,它使用std :: atomic进行无锁操作。由于该课程被广泛调用,因此影响了性能,并且遇到了麻烦。

类说明

该类类似于LIFO,但是一旦调用pop()函数,它仅返回其环形缓冲区的最后写入元素(仅当自上次pop()之后存在新元素时)。

一个线程正在调用push(),另一个线程正在调用pop()。

我读过的资料

由于这占用了我的计算机时间太多,因此我决定进一步研究std :: atomic类及其memory_order。我已经阅读了很多StackOverflow以及其他来源和书籍中的memory_order帖子,但是我无法对不同的模式有一个清晰的了解。特别是,我在获取和释放模式之间挣扎:我也看不出为什么它们与memory_order_seq_cst不同。

根据我自己的研究,我认为每个记忆顺序都是用我的话做的

memory_order_relaxed:在同一线程中,原子操作是即时的,但是其他线程可能无法立即看到最新的值,它们将需要一些时间才能被更新。编译器或OS可以自由地对代码进行重新排序。

memory_order_acquire / release:由atomic :: load使用。它防止重新排序之前存在的代码行(编译器/ OS可能在此行之后对其重新排序),并使用此线程或另一个线程中的memory_order_releasememory_order_seq_cst读取存储在此原子上的最新值。memory_order_release还可以防止对该代码重新排序之后的代码。因此,在获取/发布中,两者之间的所有代码都可以被OS改组。我不确定这是在同一线程还是不同线程之间。

memory_order_seq_cst:最容易使用,因为就像我们使用变量的自然写法一样,立即刷新其他线程加载函数的值。

LockFreeEx类

template<typename T>
class LockFreeEx
{
public:
    void push(const T& element)
    {
        const int wPos = m_position.load(std::memory_order_seq_cst);
        const int nextPos = getNextPos(wPos);
        m_buffer[nextPos] = element;
        m_position.store(nextPos, std::memory_order_seq_cst);
    }

    const bool pop(T& returnedElement)
    {

        const int wPos = m_position.exchange(-1, std::memory_order_seq_cst);
        if (wPos != -1)
        {
            returnedElement = m_buffer[wPos]; 
            return true;
        }
        else
        {
            return false;
        }
    }

private:
    static constexpr int maxElements = 8;
    static constexpr int getNextPos(int pos) noexcept {return (++pos == maxElements)? 0 : pos;}
    std::array<T, maxElements> m_buffer;
    std::atomic<int> m_position {-1};
};
Run Code Online (Sandbox Code Playgroud)

我期望它会如何改善

因此,我的第一个想法是在所有原子操作中使用memory_order_relaxed,因为pop()线程处于循环中,每10-15毫秒查找一次pop函数中的可用更新,然后允许它在第一个pop()函数中失败以实现后来有一个新的更新。仅一毫秒。

另一种选择是使用发布/获取-但我不确定它们。在所有 store()中使用release,并在所有 load()函数中获取。

不幸的是,我描述的所有memory_order似乎都可以工作,并且我不确定它们是否会失败(如果应该失败)。

最后

请,你能告诉我在这里使用宽松的内存顺序是否遇到问题?还是我应该使用发布/获取(也许对它们的进一步解释可能对我有帮助)?为什么?

我认为对于所有此类的store()或load()而言,放松是此类的最佳选择。但是我不确定!

谢谢阅读。

编辑:额外说明:

由于我看到每个人都在要求'char',因此我将其更改为int,问题已解决!但这不是我要解决的问题。

正如我之前所说,该类对于LIFO来说很可能,但是只有在最后一个元素(如果有)才重要的地方。

我有一个大的结构T(可复制和可赋值),我必须以无锁方式在两个线程之间共享。因此,我唯一知道的方法是使用一个循环缓冲区,该缓冲区写入T的最后一个已知值,以及一个原子,该原子知道写入的最后一个值的索引。如果没有,索引将为-1。

请注意,我的推线程必须知道何时有可用的“新T”,这就是pop()返回布尔值的原因。

再次感谢大家尝试协助我完成记忆订单!:)

阅读解决方案后:

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;
};
Run Code Online (Sandbox Code Playgroud)

ixS*_*Sci 5

内存顺序与您看到原子对象的某些特定更改无关,而与该更改可以保证周围的代码有关。弛豫的原子除了对原子对象本身的更改以外,不能保证其他任何事情:更改将是原子的。但是您不能在任何同步上下文中使用宽松的原子。

并且您有一些需要同步的代码。您想弹出已推送的内容,而不想弹出尚未推送的内容。因此,如果您使用轻松的操作,则不能保证您的弹出窗口会看到此推送代码:

m_buffer[nextPos] = element;
m_position.store(nextPos, std::memory_relaxed);
Run Code Online (Sandbox Code Playgroud)

如所写。同样可以这样看:

m_position.store(nextPos, std::memory_relaxed);
m_buffer[nextPos] = element;
Run Code Online (Sandbox Code Playgroud)

因此,您可能尝试从尚不存在的缓冲区中获取元素。因此,您必须使用一些同步,并且至少使用获取/释放内存顺序。


以及您的实际代码。我认为顺序可以如下:

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);
Run Code Online (Sandbox Code Playgroud)