【发布时间】:2015-09-21 09:52:37
【问题描述】:
我有一个基于循环缓冲区的无锁多生产者、单消费者队列。到目前为止,它只有非阻塞的push_back() 和pop_front() 调用。现在我想添加这些调用的阻塞版本,但我想尽量减少这对使用非阻塞版本的代码性能的影响——也就是说,它不应该把它们变成“lock-by-default ”调用。
例如阻塞 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);
}
}
}
但不幸的是,这也需要将以下块放在“非阻塞”pop_front() 的末尾:
{
std::lock_guard<mutex> lg(mux);
cv_notFull.notify_all();
}
虽然 notify 单独对性能几乎没有任何影响(如果没有线程在等待),但锁有。
所以我的问题是:
我怎样才能(如果可能,使用标准 c++14)将阻塞 push_back 和 pop_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——而不是notify 部分是否/如何避免它。但显然我没有说得足够清楚(或者人们只是不同意我的观点)。
无论如何,链接问题中的答案对我没有帮助,因为无论如何这有点像XY-problem,我决定就我遇到的实际问题提出另一个问题,从而允许更广泛的可能解决方案(也许有一种方法可以完全避免条件变量)。
This question也很相似,但是
- 它是关于 linux 上的 C 并且答案使用特定于平台的 构造(pthreads 和 futexes)
- 那里的作者要求有效的阻塞调用,但根本没有非阻塞调用。另一方面,我不太关心阻塞的效率,但希望尽可能快地保持非阻塞的效率。
【问题讨论】:
-
This question链接无效。你能修好吗?此外,head成员上的原子操作实际上并不是无锁,因为它的大小(2 * unsigned long)。 -
@Tsyvarev:谢谢,我修复了链接。
std::atomic<Idx>在 VS2015 (x64) 上无锁(只需检查std::atomic<Idx>{}.is_lock_free()),据我所知,gcc 和 clang 甚至可以对 128 位大小的数据类型(两个size_t变量)执行此操作 -
哦,刚刚发现现代x86_64支持双CAS。没关系。
标签: c++ multithreading blocking lock-free condition-variable