我有一个单链表,可以被多个线程同时访问。
实际上同时执行的唯一操作是将数据附加到列表的尾部。 现在,我使用互斥锁来确保一致性。这就是函数的样子:
void AppendCommand(BackEndCommand * beCmd)
{
Lock();
if (cmdHead == nullptr)
{
beCmd->next = nullptr;
cmdHead = beCmd;
cmdTail = beCmd;
}
else
{
cmdTail->next = beCmd;
cmdTail = beCmd;
}
++numCommands;
Unlock();
}
相关的列表数据和BackEndCommand
类型:
struct BackEndCommand {
// Command number. Any of the BackEndCommandId.
BackEndCommandId id;
// Next on the command queue or null if end of list.
struct BackEndCommand * next;
};
BackEndCommand * cmdHead = nullptr;
BackEndCommand * cmdTail = nullptr;
uint32_t numCommands = 0;
现在我很好奇在我的情况下是否可以通过某种无锁/原子操作替换该互斥锁,可能使用新的C ++ 11 atomics库或类似的? / p>
答案 0 :(得分:2)
一些答案显示了如何通过在列表的开头添加元素来完成它。也可以在列表的末尾添加元素。注意:在以下代码中,_tail_hint
并不总是指向最后一个元素,而是指向靠近尾部的元素。
我还通过添加虚拟元素简化了if
条件。
struct list
{
struct node
{
int _id;
std::atomic<node*> _next;
explicit node(int id, node* next)
: _id{id}, _next{next}
{ }
};
node _head{0, nullptr};
std::atomic<node*> _tail_hint{&_head};
void append(node* n)
{
node* tail = _tail_hint;
node* expected = nullptr;
while (!tail->_next.compare_exchange_weak(expected, n)) {
if (expected) {
tail = expected;
expected = nullptr;
}
}
_tail_hint = n;
}
};
答案 1 :(得分:1)
是的,这很有可能,只要该列表是仅添加的(至少在并发访问期间)。
如果您不按相反顺序排列列表,这是最简单的,因为那时只有一个变量需要更新(尾部),而且您不必做遍历时的原子载荷很多。
我已经从我的一个项目中调整了一些工作代码(经过彻底的单元测试)。它实现了一个只添加LIFO链表。 (请注意,为了清楚起见,我已将您的next
重命名为prev
。)
#include <atomic>
std::atomic<BackEndCommand*> tail(nullptr);
// Thread-safe
void add(BackEndCommand* element)
{
assert(element != nullptr);
auto prevTail = tail.load(std::memory_order_relaxed);
do {
element->prev = prevTail;
} while (!tail.compare_exchange_weak(prevTail, element, std::memory_order_release, std::memory_order_relaxed));
}
// Thread-safe
void iterate()
{
for (auto ptr = tail.load(std::memory_order_acquire); ptr != nullptr; ptr = ptr->prev) {
// Do something with ptr
}
}
答案 2 :(得分:1)
是的,你可以
以下是来自的示例代码 http://en.cppreference.com/w/cpp/atomic/atomic/compare_exchange
您甚至可以在网页上运行它
#include <atomic>
template<typename T>
struct node
{
T data;
node* next;
node(const T& data) : data(data), next(nullptr) {}
};
template<typename T>
class stack
{
std::atomic<node<T>*> head;
public:
void push(const T& data)
{
node<T>* new_node = new node<T>(data);
// // put the current value of head into new_node->next
// new_node->next = head.load(std::memory_order_relaxed);
//
// // now make new_node the new head, but if the head
// // is no longer what's stored in new_node->next
// // (some other thread must have inserted a node just now)
// // then put that new head into new_node->next and try again
// while(!head.compare_exchange_weak(new_node->next, new_node,
// std::memory_order_release,
// std::memory_order_relaxed))
// ; // the body of the loop is empty
//
// Note: the above use is not thread-safe in at least
// GCC prior to 4.8.3 (bug 60272), clang (bug 18899), MSVC (bug 819819).
// The following is a workaround:
node<T>* old_head = head.load(std::memory_order_relaxed);
do {
new_node->next = old_head;
} while(!head.compare_exchange_weak(old_head, new_node,
std::memory_order_release,
std::memory_order_relaxed));
}
};
int main()
{
stack<int> s;
s.push(1);
s.push(2);
s.push(3);
}
答案 3 :(得分:1)
是的,你可以。您需要使用atomic_compare_exchange_weak()
或atomic_compare_exchange_strong()
将前一个尾部的next
指针与指向新元素的指针交换。算法如下:
设置新元素,将其next
指针设置为零。重要的是,在调用atomic<>
魔法之前完成此操作。
确定列表中的最后一个元素。使用你喜欢的任何方法。确保其next
指针为nullptr
。让我们称之为oldTail
。
执行atomic_compare_exchange_xxx()
,指明您希望oldTail->next
仍为nullptr
。这可能会失败。如果是这样,其他一些进程会在您不查看时将一个元素附加到列表中。在这种情况下,您需要返回2以确定新尾部。
如果atomic_compare_exchange_xxx()
成功,您就完成了。
此方法要求列表末尾仅附加。它还要求列表元素保持有效,只要另一个线程可以保存指向它的指针以便附加另一个元素。确保这很棘手,特别是如果你试图以无锁方式进行,但它是可能的(可以通过RCU模式实现,见http://en.wikipedia.org/wiki/Read-copy-update)。