一、无锁数据结构定义
1. 无锁数据结构
- 一个以上的线程必须能够同时访问该数据结构,他们不必执行相同的操作
- 当其中一个访问线程被调度器中途挂起时,其他线程必须能够继续完成自己的工作, 而无需等待挂起线程
2. 无等待数据结构
- 是一种无锁数据结
- 具有附加属性,即访问该数据结构的每个线程都可以在一定数量的步骤内完成其操作,而与其他线程的行为无关。因此,由于与其他线程的冲突而可能涉及无数次重试的算法并非没有等待时间。
二、无锁数据结构的利与弊
1. 无锁数据结构的利
- 主要原因: 将并发最大化。
使用基于锁的容器, 会让线程阻塞或等待; 互斥锁削弱了结构的并发性。 在无锁数据结构中, 某些线程可以逐步执行。 在无等待数据结构中, 无论其他线程当时在做什么, 每一个线程都可以转发进度。 - 健壮性.
如果线程在持有锁的同时死亡,那么该数据结构将永远被破坏。但是,如果线程在对无锁数据结构的操作中途中途死亡,则除了该线程的数据外,什么都不会丢失。其他线程可以正常进行。 - 死锁活锁问题
死锁问题不会困扰无锁数据结构; 无等待的代码不会被活锁所困扰,因其操作执行步骤是有上限的。
2. 无锁数据结构的弊
虽然提高了并发访问的能力, 减少了单个线程的等待时间, 但是其可能会将整体性能拉低。下面是原因:
- 原子操作的无锁代码要慢于无原子操作的代码,原子操作就相当于无锁数据结构中的锁。
- 不仅如此, 硬件必须通过同一个原子变量对线程间的数据进行同步。
三、设计无锁数据结构的指导建议
1. 使用std::memory_order_seq_cst的原型.
std::memory_order_seq_cst 比起其他内存序要简单的多,因为所有操作都将其作为总序。本章的所有例子,都是从 std::memory_order_seq_cst 开始,只有当基本操作正常工作的时候,才放宽内存序的选择。在这种情况下,使用其他内存序就是进行优化(早起可以不用这样做).
2. 对无锁内存的回收策略
这里与无锁代码最大的区别就是内存管理。当有其他线程对节点进行访问的时候,节点无法被任一线程删除;为避免过多的内存使用,还是希望这个节点在能删除的时候尽快删除。
有三种技术来保证内存可以被安全的回收:
- 等待无线程对数据结构进行访问时,删除所有等待删除的对象。
- 使用风险指针来标识正在被线程访问的对象。
- 对对象进行引用计数,当没有线程对对象进行引用时,将其删除。
在所有例子中,主要的想法都是使用一种方式去跟踪指定对象上的线程访问数量,当没有现成对对象进行引用的时候,将对象删除。当然,在无锁数据结构中,还有很多方式可以用来回收内存。例如,理想情况下使用一个垃圾收集器。比起算法来说,其实现更容易一些。只需要让回收器知道,当节点没被引用的时候,回收节点,就可以了。
3. 小心ABA问题
- "ABA问题"的产生
循环使用节点,只在数据结构被销毁的时候才将节点完全删除。因为节点能被复用,那么就不会有非法的内存,所以这就能避免未定义行为的发生。这种方式的缺点:产生“ABA问题”.
"ABA问题”在使用释放链表和循环使用节点的算法中很是普遍,而将节点返回给分配器,则不会引起这个问题。 - 其流程是:
线程1读取原子变量x,并且发现其值是A。
线程1对这个值进行一些操作,比如,解引用(当其是一个指针的时候),或做查询,或其他操作。
操作系统将线程1挂起。
其他线程对x执行一些操作,并且将其值改为B。
另一个线程对A相关的数据进行修改(线程1持有),让其不再合法。可能会在释放指针指向的内存时,代码产生剧烈的反应(大问题);或者只是修改了相关值而已(小问题)。
再来一个线程将x的值改回为A。如果A是一个指针,那么其可能指向一个新的对象,只是与旧对象共享同一个地址而已。
线程1继续运行,并且对x执行“比较/交换”操作,将A进行对比。这里,“比较/交换”成功(因为其值还是A),不过这是一个 错误的A ( the wrong A value )。从第2步中读取的数据不再合法,但是线程1无法言明这个问题,并且之后的操作将会损坏数据结构。 - 解决ABA问题方法
让变量x中包含一个ABA计数器。“比较/交换”会对加入计数器的x进行操作。每次的值都不一样,计数随之增长,所以在x还是原值的前提下,即使有线程对x进行修改,“比较/交换”还是会失败。
4. 识别忙等待循环和帮助其他线程
线程在执行push操作时,可能必须等待另一个push操作流程的完成。等待线程就会被孤立,将会陷入到忙等待循环中,当线程尝试失败的时候,会继续循环,这样就会浪费CPU的计算周期。当忙等待循环结束时,就像一个阻塞操作解除,和使用互斥锁的行为一样。通过对算法的修改,当之前的线程还没有完成操作前,让等待线程执行未完成的步骤,就能让忙等待的线程不再被阻塞。在队列中,需要将一个数据成员转换为一个原子变量,而不是使用非原子变量和使用“比较/交换”操作来做这件事;要是在更加复杂的数据结构中,这将需要更加多的变化来满足需求。
四、无锁数据结构的例子
1. 无锁栈
- 借助无锁std::shared_ptr<>的实现,实现无锁栈
github示例代码
#include <iostream>
#include <atomic>
#include <memory>
#include <thread>
#include <chrono>
#include <random>
/*
使用无锁std::shared_ptr<>的实现:std::atomic_is_lock_free(&some_shared_ptr)返回true
*/
template<typename T>
class LockFreeStack
{
private:
struct node
{
std::shared_ptr<T> data;
std::shared_ptr<node> next;
node(T const& data_) :
data(std::make_shared<T>(data_))
{}
};
std::shared_ptr<node> head;
public:
void push(T const& data)
{
std::shared_ptr<node> const new_node = std::make_shared<node>(data);
new_node->next = head;
while (!std::atomic_compare_exchange_weak(
&head, &new_node->next, new_node));
}
std::shared_ptr<T> pop()
{
std::shared_ptr<node> old_head = std::atomic_load(&head);
while (old_head && !std::atomic_compare_exchange_weak(
&head, &old_head, old_head->next));
return old_head ? old_head->data : std::shared_ptr<T>();
}
};
void PushThreadFun(LockFreeStack<int>& stack) {
static thread_local int i = 1;
std::random_device r;
std::default_random_engine e(r());
std::uniform_int_distribution<int> uniform_dist(1, 3);
while (1) {
std::cout << "PushThreadFun id:" << std::this_thread::get_id()<<" value:"<<i << std::endl;
stack.push(i++);
std::this_thread::sleep_for(std::chrono::seconds(uniform_dist(e)));
}
}
void GetThreadFun(LockFreeStack<int>& stack) {
while (1) {
auto res = stack.pop();
if (res.get() != nullptr) {
std::cout << "GetThreadFun id:" << std::this_thread::get_id() << " value:" << *res << std::endl;
}
}
}
int main()
{
LockFreeStack<int> stack;
std::thread pushT1(PushThreadFun,std::ref(stack));
std::thread pushT2(PushThreadFun, std::ref(stack));
std::thread getT1(GetThreadFun, std::ref(stack));
std::thread getT2(GetThreadFun, std::ref(stack));
pushT1.join();
pushT2.join();
getT1.join();
getT2.join();
std::cout << "Hello World!\n";
}
- 借助引用计数思想,实现无锁栈
github示例代码
#include <iostream>
#include <atomic>
#include <memory>
#include <thread>
#include <chrono>
#include <random>
template<typename T>
class LockFreeStack
{
private:
struct node;
struct counted_node_ptr
{
int external_count{0};
node* ptr{nullptr};
};
struct node
{
std::shared_ptr<T> data;
std::atomic<int> internal_count;
counted_node_ptr next;
node(T const& data_) :
data(std::make_shared<T>(data_)),
internal_count(0)
{}
};
std::atomic<counted_node_ptr> head;
void increase_head_count(counted_node_ptr& old_counter)
{
counted_node_ptr new_counter;
do
{
new_counter = old_counter;
++new_counter.external_count;
} while (!head.compare_exchange_strong(
old_counter, new_counter,
std::memory_order_acquire,
std::memory_order_relaxed));
old_counter.external_count = new_counter.external_count;
}
public:
~LockFreeStack()
{
if (head.load(std::memory_order_relaxed).ptr != nullptr) {
while (pop());
}
}
void push(T const& data)
{
counted_node_ptr new_node;
new_node.ptr = new node(data);
new_node.external_count = 1;
new_node.ptr->next = head.load(std::memory_order_relaxed);
while (!head.compare_exchange_weak(
new_node.ptr->next, new_node,
std::memory_order_release,
std::memory_order_relaxed));
}
std::shared_ptr<T> pop()
{
counted_node_ptr old_head =
head.load(std::memory_order_relaxed);
for (;;)
{
increase_head_count(old_head);
node* const ptr = old_head.ptr;
if (!ptr)
{
return std::shared_ptr<T>();
}
if (head.compare_exchange_strong(
old_head, ptr->next, std::memory_order_relaxed))
{
std::shared_ptr<T> res;
res.swap(ptr->data);
int const count_increase = old_head.external_count - 2;
if (ptr->internal_count.fetch_add(
count_increase, std::memory_order_release) == -count_increase)
{
delete ptr;
}
return res;
}
else if (ptr->internal_count.fetch_add(
-1, std::memory_order_relaxed) == 1)
{
ptr->internal_count.load(std::memory_order_acquire);
delete ptr;
}
}
}
};
void PushThreadFun(LockFreeStack<int>& stack) {
static thread_local int i = 1;
std::random_device r;
std::default_random_engine e(r());
std::uniform_int_distribution<int> uniform_dist(1, 3);
while (1) {
std::cout << "PushThreadFun id:" << std::this_thread::get_id()<<" value:"<<i << std::endl;
stack.push(i++);
std::this_thread::sleep_for(std::chrono::seconds(uniform_dist(e)));
}
}
void GetThreadFun(LockFreeStack<int>& stack) {
while (1) {
auto res = stack.pop();
if (res.get() != nullptr) {
std::cout << "GetThreadFun id:" << std::this_thread::get_id() << " value:" << *res << std::endl;
}
}
}
int main()
{
LockFreeStack<int> stack;
std::thread pushT1(PushThreadFun,std::ref(stack));
std::thread pushT2(PushThreadFun, std::ref(stack));
std::thread getT1(GetThreadFun, std::ref(stack));
std::thread getT2(GetThreadFun, std::ref(stack));
pushT1.join();
pushT2.join();
getT1.join();
getT2.join();
}
2. 无锁队列
- SPSC队列
github示例代码
#include <iostream>
#include <memory>
#include <atomic>
#include <thread>
#include <chrono>
#include <random>
/*
“单生产者,单消费者”(single-producer,single-consumer SPSC)队列
*/
template<typename T>
class LockFreeQueue
{
private:
struct node
{
std::shared_ptr<T> data;
node* next;
node() :
next(nullptr)
{}
};
std::atomic<node*> head;
std::atomic<node*> tail;
node* pop_head()
{
node* const old_head = head.load();
if (old_head == tail.load()) // 1
{
return nullptr;
}
head.store(old_head->next);
return old_head;
}
public:
LockFreeQueue() :
head(new node), tail(head.load())
{}
LockFreeQueue(const LockFreeQueue& other) = delete;
LockFreeQueue& operator=(const LockFreeQueue& other) = delete;
~LockFreeQueue()
{
while (node* const old_head = head.load())
{
head.store(old_head->next);
delete old_head;
}
}
std::shared_ptr<T> pop()
{
node* old_head = pop_head();
if (!old_head)
{
return std::shared_ptr<T>();
}
std::shared_ptr<T> const res(old_head->data);
delete old_head;
return res;
}
void push(T new_value)
{
std::shared_ptr<T> new_data(std::make_shared<T>(new_value));
node* p = new node;
node* const old_tail = tail.load();
old_tail->data.swap(new_data);
old_tail->next = p;
tail.store(p);
}
};
void PushThreadFun(LockFreeQueue<int>& queue) {
static thread_local int i = 1;
std::random_device r;
std::default_random_engine e(r());
std::uniform_int_distribution<int> uniform_dist(1, 3);
while (1) {
std::cout << "PushThreadFun id:" << std::this_thread::get_id() << " value:" << i << std::endl;
queue.push(i++);
std::this_thread::sleep_for(std::chrono::seconds(uniform_dist(e)));
}
}
void GetThreadFun(LockFreeQueue<int>& queue) {
while (1) {
auto res = queue.pop();
if (res.get() != nullptr) {
std::cout << "GetThreadFun id:" << std::this_thread::get_id() << " value:" << *res << std::endl;
}
}
}
int main()
{
LockFreeQueue<int> queue;
std::thread pushT(PushThreadFun, std::ref(queue));
std::thread getT(GetThreadFun, std::ref(queue));
pushT.join();
getT.join();
}
- MPMC队列
github开源项目