介绍
无锁数据结构,即不使用任何锁机制实现可以多线程并发访问的数据结构。相比较有锁数据结构有如下优缺点:
优点:
- 最大限度实现并发:在基于锁的容器上,若某个线程还未完成操作,就大有可能阻塞另一线程,使之陷入等待而无法处理;而且,互斥锁的根本意图是杜绝并发功能。在无锁数据结构上,总是存在某个线程能执行下一操作。
- 提高代码的健壮性:假设数据结构的写操作受锁保护,如果线程在持锁期间终止,那么该数据结构只完成了部分改动,且此后无从修补。但是对于无锁数据结构,若某线程操作无锁数据时意外终结,则丢失的数据仅限于它持有的部分,其他数据依然完好,能被别的线程正常处理。
无锁数据结构不含锁,因此不会出现死锁
缺点:
- 执行效率低:对线程安全的无锁数据结构执行修改操作,难度远远高于对带锁的数据结构体执行修改操作,因为代码实现比有锁数据结构更加复杂,同时里面包含大量的原子操作,原子操作相对执行效率较低,所以无锁结构虽然提高了某个数据结构的访问效率,但是程序整体性能可能降低。
实现无锁数据结构,最好不要一开始就直接想着去如何实现无锁数据结构,因为这样能难考虑全面,可以以最简单的单线程栈开始,然后慢慢修改成无锁栈,下面以最简单的单线栈开始。
1)单线程栈实现
这里的栈用简单的单向链表来实现,栈顶为链表头部,通过操作链表头节点来完成栈的push和pop操作。
- 单线程栈
template <typename T>
class lock_free_stack
{
private:
struct node
{
shared_ptr<T> data;
node *next;
node(T const &data_) : data(make_shared<T>(data_)), next(nullptr) {}
};
std::atomic<node *> head = nullptr;
public:
void push(T const &data)
{
node *const new_node = new node(data);
new_node->next = head.load(); // op1
head = new_node; // op2
}
shared_ptr<T> pop()
{
node *old_head = head.load();
if (!old_head)
{
return nullptr
}
head = old_head->next; // op3
shared_ptr<T> result = old_head->data;
delete old_head;
return result;
}
};
把上面的代码用在多线程中可以发现一下问题:
head是临界资源,所有访问head的操作都会存在资源竞争,在push()中op1加载了head的值,但在op2操作时head可能已发生变化(如:其它线程也调用了push()并执行完成),这时可能造成node资源泄漏。同理op3也会存在相同问题。
下考虑使用CAS来解决这个问题。
2)CAS实现线程安全栈
解决方案:把上面代码中的op2和op3改为CAS操作,这样即使head被其它线程修改,CAS操作也会保证当前线程的head始终是修改后的最新值,实现代码如下:
- CAS实现线程安全栈
template <typename T>
class lock_free_stack
{
private:
struct node
{
shared_ptr<T> data;
node *next;
node(T const &data_) : data(make_shared<T>(data_)), next(nullptr) {}
};
std::atomic<node *> head = nullptr;
public:
void push(T const &data)
{
node *const new_node = new node(data);
new_node->next = head.load();
while(!head.compare_exchange_weak(new_node->next, new_node)); // op1
}
shared_ptr<T> pop()
{
node *old_head = head.load();
while(old_head && !head.compare_exchange_weak(old_head, old_head->next)); // op3
shared_ptr<int> result = old_head ? old_head->data : nullptr;
delete old_head;
return result;
}
};
上面使用CAS操作保证了head可以是最新值,如果head和*_weak的第一个参数不等时,会自动更新第一个参数的值。目前看push和pop都是线程安全的,但是在pop中op3还是存在问题,如果有其它线程的old_head和当前线程指向同一个节点,并且其它线程已经释放了old_head,那么如果当前线程执行到op3这里时,old_head→next就访问就会出错。需要解决这个问题就必须保证delete old_head
时,其它线程没有访问到这个节点,即有多个线程同时调用pop时不能释放old_head,下面加入引用计数来避免这种情况。
3)使用引用计的实现
因为问题出在pop函数,push函数没有任何问题,下面就省略掉push函数。
修改后的pop函数:
template <typename T>
class lock_free_stack
{
private:
std::atomic<unsigned> threads_in_pop; // 1 原子变量
void try_reclaim(node *old_head);
public:
std::shared_ptr<T> pop()
{
++threads_in_pop; // 2 在做事之前,计数值加1
node *old_head = head.load();
while (old_head &&
!head.compare_exchange_weak(old_head, old_head->next))
;
std::shared_ptr<T> res;
if (old_head)
{
res.swap(old_head->data); // 3 从节点中直接提取数据,而不拷贝指针
}
try_reclaim(old_head); // 4 回收删除的节点
return res;
}
}
try_reclaim实现:
template<typename T>
class lock_free_stack
{
private:
std::atomic<node *> to_be_deleted; // 待删除列表
static void delete_nodes(node *nodes)
{
while (nodes)
{
node *next = nodes->next;
delete nodes;
nodes = next;
}
}
void try_reclaim(node *old_head)
{
if (threads_in_pop == 1) // 1 当只有一个线程调用pop时那么当前的old_head一定是可以删除的
{
// 2 “可删除”列表节点交给nodes_to_delete,to_be_deleted接管后面新来的节点
node *nodes_to_delete = to_be_deleted.exchange(nullptr);
// 3 这里再判断一遍是因为exchange之前可能又有其它线程调用pop,
// 有可能导致当前删除列表中有的节点有多个引用
if (!--threads_in_pop)
{
delete_nodes(nodes_to_delete); // 4 删除列表中的所有node
}
// 5 列表中可能存在多个引用的节点,需要重新加入到to_be_deleted待删除列表
else if (nodes_to_delete)
{
chain_pending_nodes(nodes_to_delete); // 6
}
// 7 当前的node可以安全删除,因为即使threads_in_pop增加也只会影响后面的节点
delete old_head;
}
else
{
chain_pending_node(old_head); // 8 有多个引用就加入待删除列表
--threads_in_pop;
}
}
void chain_pending_nodes(node *nodes)
{
node *last = nodes;
while (node *const next = last->next) // 9 让next指针指向链表的末尾
{
last = next;
}
chain_pending_nodes(nodes, last);
}
void chain_pending_nodes(node *first, node *last)
{
last->next = to_be_deleted; // 10
while (!to_be_deleted.compare_exchange_weak( // 11 用循环来保证last->next的正确性
last->next, first));
}
void chain_pending_node(node *n)
{
chain_pending_nodes(n, n); // 12
}
};
以上实现虽然解决了node可以安全删除的问题,但是在高负荷的情况下,多个线程频繁调用pop()可能会出现删除列表节点数量无限增加,最终还是会出现资源泄漏。后面引入风险指针来解决这个问题。
4)风险指针介绍
这里先了解下什么是风险指针,风险指针的基本原理:
当有线程去访问要被(其他线程)删除的对象时,会先设置对这个对象设置一个风险指针,而后通知其他线程,删除这个指针是一个危险的行为。一旦这个对象不再被需要,那么就可以清除风险指针了。即当线程想要删除一个对象,那么它就必须检查系统中其他线程是否持有风险指针。当没有风险指针的时候,那么它就可以安全删除对象。否则,它就必须等待风险指针的消失了。这样,线程就得周期性的检查其想要删除的对象是否能安全删除。下面式风险指针实现:
unsigned const max_hazard_pointers = 100;
struct hazard_pointer
{
std::atomic<std::thread::id> id;
std::atomic<void *> pointer;
};
hazard_pointer hazard_pointers[max_hazard_pointers]; // 存储风险指针
// 风险指针类
class hp_owner
{
hazard_pointer *hp;
public:
hp_owner(hp_owner const &) = delete;
hp_owner operator=(hp_owner const &) = delete;
hp_owner() : hp(nullptr)
{
for (unsigned i = 0; i < max_hazard_pointers; ++i)
{
std::thread::id old_id;
if (hazard_pointers[i].id.compare_exchange_strong( // 尝试声明风险指针的所有权
old_id, std::this_thread::get_id()))
{
hp = &hazard_pointers[i];
break;
}
}
if (!hp) // 可用的hazard_pointer用完了
{
throw std::runtime_error("No hazard pointers available");
}
}
std::atomic<void *> &get_pointer()
{
return hp->pointer;
}
~hp_owner()
{
hp->pointer.store(nullptr);
hp->id.store(std::thread::id());
}
};
// 实例化一个风险指针
std::atomic<void *> &get_hazard_pointer_for_current_thread()
{
thread_local static hp_owner hazard; // 每个线程都有自己的风险指针
return hazard.get_pointer();
}
// 查找该指针是否由其它线程在引用
bool outstanding_hazard_pointers_for(void *p)
{
for (unsigned i = 0; i < max_hazard_pointers; ++i)
{
if (hazard_pointers[i].pointer.load() == p)
{
return true;
}
}
return false;
}
风险是指实现原理是用一个全局可访问的数组来存放所有线程的当前使用的风险指针,每个线程可以通过查找这个数组中记录的指针地址来判断该指针是否有其它线程也在使用。
5)使用风险指针的实现
下面的代码是将风险指针应用到无锁栈的pop()中:
template <typename T>
std::shared_ptr<T> pop()
{
// 实例化一个风险指针
std::atomic<void *> &hp = get_hazard_pointer_for_current_thread();
node *old_head = head.load();
do
{
node *temp;
do // 直到将风险指针设为head指针
{
temp = old_head;
hp.store(old_head);
old_head = head.load();
} while (old_head != temp);
} while (old_head && !head.compare_exchange_strong(old_head, old_head->next));
hp.store(nullptr); // 当声明完成,清除风险指针
std::shared_ptr<T> res;
if (old_head)
{
res.swap(old_head->data);
// 在删除之前对风险指针引用的节点进行检查
if (outstanding_hazard_pointers_for(old_head))
{
reclaim_later(old_head);
}
else
{
delete old_head;
}
delete_nodes_with_no_hazards();
}
return res;
}
unsigned const max_hazard_pointers = 100;
struct hazard_pointer
{
std::atomic<std::thread::id> id;
std::atomic<void *> pointer;
};
hazard_pointer hazard_pointers[max_hazard_pointers];
// 风险指针类
class hp_owner
{
hazard_pointer *hp;
public:
hp_owner(hp_owner const &) = delete;
hp_owner operator=(hp_owner const &) = delete;
hp_owner() : hp(nullptr)
{
for (unsigned i = 0; i < max_hazard_pointers; ++i)
{
std::thread::id old_id;
// 尝试声明风险指针的所有权
if (hazard_pointers[i].id.compare_exchange_strong(
old_id, std::this_thread::get_id()))
{
hp = &hazard_pointers[i];
break;
}
}
if (!hp) // 可用的hazard_pointer用完了
{
throw std::runtime_error("No hazard pointers available");
}
}
std::atomic<void *> &get_pointer()
{
return hp->pointer;
}
~hp_owner()
{
hp->pointer.store(nullptr);
hp->id.store(std::thread::id());
}
};
// 实例化一个风险指针
std::atomic<void *> &get_hazard_pointer_for_current_thread()
{
thread_local static hp_owner hazard; // 每个线程都有自己的风险指针
return hazard.get_pointer();
}
// 查找该指针是否由其它线程在引用
bool outstanding_hazard_pointers_for(void *p)
{
for (unsigned i = 0; i < max_hazard_pointers; ++i)
{
if (hazard_pointers[i].pointer.load() == p)
{
return true;
}
}
return false;
}
template <typename T>
void do_delete(void *p)
{
delete static_cast<T *>(p);
}
// 待回收数据节点
struct data_to_reclaim
{
void *data;
std::function<void(void *)> deleter;
data_to_reclaim *next;
template <typename T>
data_to_reclaim(T *p) : data(p),
deleter(&do_delete<T>),
next(0) {}
~data_to_reclaim()
{
deleter(data);
}
};
std::atomic<data_to_reclaim *> nodes_to_reclaim; // 维护待回收数据列表
// 添加到回收列表中
void add_to_reclaim_list(data_to_reclaim *node)
{
node->next = nodes_to_reclaim.load();
while (!nodes_to_reclaim.compare_exchange_weak(node->next, node));
}
template <typename T>
// 把data移到回收列表中稍后回收
void reclaim_later(T *data)
{
add_to_reclaim_list(new data_to_reclaim(data));
}
void delete_nodes_with_no_hazards()
{
// 把nodes_to_reclaim中的所有待删除列表移交给current去释放
data_to_reclaim *current = nodes_to_reclaim.exchange(nullptr);
while (current)
{
data_to_reclaim *const next = current->next;
// 没有其它线程引用就直接释放
if (!outstanding_hazard_pointers_for(current->data))
{
delete current;
}
else // 否则再添加到nodes_to_reclaim维护的列表中
{
add_to_reclaim_list(current);
}
current = next;
}
}
有风险指针实现的无锁栈相比第3节中的实现,上面的程序每次pop都有机会检查所有删除列表中的节点是否可以删除,不会受到当前pop有多个线程调用的影响,这样实现避免了高负荷情况下多线程同时调用pop以至于没有机会释放列表节点资源的情况。
上面的代码使用的是一个数组存储风险指针,可以优化为哈希表来存储,可以加快节点的搜索。
6)使用无锁 shared_ptr的实现
我们知道上面的代码都是在解决节点删除的问题,为了实现这个功能避免内存泄漏,单独维护了一个删除列表,但是实现太过复杂,代码量也较大。下面考虑使用shared_ptr来管理node,可以把待删除节点列表省略掉:
使用无锁 shared_ptr 的实现:
class lock_free_stack
{
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 = std::atomic_load(&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>();
}
};
这样实现虽然简单,但是每个node都是shared_ptr类型,在一些情况下,使用 std::shared_ptr<> 实现的结构并非无锁,这就需要手动管理shared_ptr引用计数。