将阻塞函数添加到无锁队列

时间:2015-09-21 09:52:38

标签: c++ multithreading blocking lock-free condition-variable

我有一个基于循环缓冲区的无锁多生产者,单个消费者队列。到目前为止,它只有非阻塞push_back()pop_front()来电。现在我想添加这些调用的阻止版本,但我希望尽量减少这对使用非阻塞版本的代码性能的影响 - 也就是说,它不应该将它们变成" lock-通过默认"调用

E.g。最简单的阻塞push_back()版本如下所示:

void push_back_Blocking(const T& pkg) {
    if (!push_back(pkg)) {
        unique_lock<mutex> ul(mux);
        while (!push_back(pkg)) {
            cv_notFull.wait(ul);
        }
    }
}

但不幸的是,这还需要将以下块放在&#34;非阻塞&#34; pop_front()

{
    std::lock_guard<mutex> lg(mux);
    cv_notFull.notify_all();
}

虽然notify本身几乎没有任何性能影响(如果没有线程在等待),则锁具有。

所以我的问题是:
我如何(如果可能,使用标准c ++ 14)将阻塞push_backpop_front成员函数添加到我的队列中,而不会严重阻碍non_blocking对应的性能(读取:最小化系统调用) ?至少只要没有线程实际被阻止 - 但理想情况下即使这样。

作为参考,我当前的版本与此类似(我省略了调试检查,数据对齐和显式内存排序):

template<class T, size_t N>
class MPSC_queue {
    using INDEX_TYPE = unsigned long;
    struct Idx {
        INDEX_TYPE idx;
        INDEX_TYPE version_cnt;
    };
    enum class SlotState {
        EMPTY,
        FILLED
    };
    struct Slot {
        Slot() = default;               
        std::atomic<SlotState> state= SlotState::EMPTY;
        T data{};
    };
    struct Buffer_t {
        std::array<Slot, N> data{}; 
        Buffer_t() {
            data.fill(Slot{ SlotState::EMPTY, T{} });
        }
        Slot& operator[](Idx idx) {
            return this->operator[](idx.idx);
        }
        Slot& operator[](INDEX_TYPE idx) {
            return data[idx];                   
        }
    };

    Buffer_t buffer;
    std::atomic<Idx> head{};
    std::atomic<INDEX_TYPE> tail=0;

    INDEX_TYPE next(INDEX_TYPE old) { return (old + 1) % N; }

    Idx next(Idx old) {
        old.idx = next(old.idx);
        old.version_cnt++;
        return old;
    }
public:     
    bool push_back(const T& val) {
        auto tHead = head.load();
        Idx wrtIdx;
        do {
            wrtIdx = next(tHead);
            if (wrtIdx.idx == tail) {
                return false;
            }
        } while (!head.compare_exchange_strong(tHead, wrtIdx));

        buffer[wrtIdx].data = val;
        buffer[wrtIdx].state = SlotState::FILLED;
        return true;
    }

    bool pop_front(T& val) {                
        auto rIdx = next(tail);
        if (buffer[rIdx].state != SlotState::FILLED) {
            return false;
        }
        val = buffer[rIdx].data;
        buffer[rIdx].state = SlotState::EMPTY;
        tail = rIdx;
        return true;
    }
};

相关问题:

我问了一个类似的问题,特别是关于优化condition_variable::notify here的使用情况,但这个问题已被关闭为this question的假设副本。
我不同意,因为这个问题是关于为什么一般情况下变量需要互斥量(或者更确切地说是它的pthread等价物) - 关注condition_variable::wait - 而不是{如果/如何避免{ {1}}部分。但显然我没有做到足够明确(或者人们只是不同意我的意见)。

在任何情况下,链接问题中的答案都没有帮助我,因为这有点像XY-problem,我决定再问一个关于我所遇到的实际问题的问题,从而允许更广泛的可能的解决方案(也许有一种方法可以完全避免条件变量)。

This question也非常相似,但

  1. 它是关于Linux上的C,答案使用特定于平台 构造(pthreads和futexes)
  2. 那里的作者要求进行高效的阻止呼叫,但根本没有阻止呼叫。另一方面,我不太关心阻塞效率,但希望尽快保持非阻塞效率。

1 个答案:

答案 0 :(得分:2)

如果条件变量上有潜在服务员,则 锁定ViewCell来电的互斥锁。

事情是条件检查RowHeight)在等待条件变量之前执行(C ++ 11提供)没有其它的方法)。因此,互斥是唯一可以保证这些行为之间保持一致的手段。

但是,如果没有潜在的服务员,可以省略锁定(和通知)。只需使用additinal flag:

notify_all

这里的关键是:

  1. !push_back(pkg)设置标记并在class MPSC_queue { ... // Original definitions std::atomic<bool> has_waiters; public: void push_back_Blocking(const T& pkg) { if (!push_back(pkg)) { unique_lock<mutex> ul(mux); has_waiters.store(true, std::memory_order_relaxed); // #1 while (!push_back(pkg)) { // #2 inside push_back() method cv_notFull.wait(ul); // Other waiter may clean flag while we wait. Set it again. Same as #1. has_waiters.store(true, std::memory_order_relaxed); } has_waiters.store(false, std::memory_order_relaxed); } } // Method is same as original, exposed only for #2 mark. bool push_back(const T& val) { auto tHead = head.load(); Idx wrtIdx; do { wrtIdx = next(tHead); if (wrtIdx.idx == tail) { // #2 return false; } } while (!head.compare_exchange_strong(tHead, wrtIdx)); buffer[wrtIdx].data = val; buffer[wrtIdx].state = SlotState::FILLED; return true; } bool pop_front(T& val) { // Main work, same as original pop_front, exposed only for #3 mark. auto rIdx = next(tail); if (buffer[rIdx].state != SlotState::FILLED) { return false; } val = buffer[rIdx].data; buffer[rIdx].state = SlotState::EMPTY; tail = rIdx; // #3 // Notification part if(has_waiters.load(std::memory_order_relaxed)) // #4 { // There are potential waiters. Need to lock. std::lock_guard<mutex> lg(mux); cv_notFull.notify_all(); } return true; } }; 处查看#1的检查条件。
  2. tail存储在#2并在tail处检查标记。
  3. 这两种关系都应该暴露某种通用订单。那个#3应该在#4之前观察,甚至是其他线程。 #1#2相同。

    在这种情况下,可以保证,如果检查标记#3发现它未设置,那么可能的进一步条件检查#4将发现条件更改的效果#4 。因此,不锁定(和通知)是安全的,因为不可能有服务员。

    在您当前的实施中,#2#3之间的通用订单是通过加载#1隐式 memory_order_seq_cst 来提供的。通过使用隐式 memory_order_seq_cst 存储#2来提供tail#3之间的相同顺序。

    在这种方法中,“如果没有服务员就不要锁定”,通用订单是最棘手的部分。在这两种关系中,它是 Read After Write 顺序,使用 memory_order_acquire memory_order_release 的任意组合都无法实现。所以应该使用 memory_order_seq_cst