C++并发实战 lock-free的实现

不使用锁实现一个线程安全的栈
基础的栈是相对简单的,节点取回的顺序和加入的顺序相反(LIFO),因此重要的是能够保证一个元素加入到栈,能够被其他线程快速的取走,
同样重要的是保证只有一个线程能够返回指定元素。最简单的栈就是一个链表,头指针确定了第一个节点(也是下一个被取走的节点),每个节点都能指向下一个节点


下面的方案,添加一个节点是相对容易的
1 创建一个节点
2 将该节点的next指针指向当前头结点
3 设置头结点指向该节点


在一个单线程的上下文环境中会工作的很好,但是如果有另外的线程修改栈就不够了。关键是如果有两个线程都添加节点,就会存在竞争条件发生在23之间,当你的线程读头节点值并且更新值时,第二个线程会修改头节点的值,这会导致现成的修改丢失甚至更糟糕的结果。需要注意的是,一旦头节点被更新成你的新节点,其他线程应该能读到这个节点,因此你的新节点一旦已经设置好(头节点设置为新节点时),之后不应该被修改。

template<typename T>
class lock_free_stack
{
private:
struct node {
T data;
node* next;
node(T const& data_): data(data_) {}       1
};


std::atomic<node*> head;
public:
void push(T const& data) {
node* const new_node=new node(data); 2
new_node->next=head.load(); 3
while(!head.compare_exchange_weak(new_node->next,new_node)); 4
}
};

使用compare_exchange_weak 保证了当把头指针存储在new_node->next时与头指针与new_node->next始终相等,这段代码使用了类CAS函数。
返回失败意味着比较失败(例如因为头指针被别的线程改变),这时第一个参数(new_node->next)将被更新成当前的头指针。这时编译器会在每次循环中重新加载头指针,程序员不需要做这件事了,同时,因为通过使用compare_exchange_weak循环来判断失败,在一些架构上会比compare_exchange_strong产生更优化的代码


现在还没有pop操作,但是可以核对push操作。唯一会抛出异常的地方时构造节点的时候,但是之后会被清理,而且这个链表本身不会被修改,所以是很安全的,因为构建数据并存储作为节点的一部分,可以使用compare_exchange_weak()来更新头指针,这里没有竞争条件的问题。一旦CAS操作成功,这个节点就已经在列表上能够被提取了。整个过程没有锁,所以也不会有死锁的可能性,同时push操作能够成功通过测试。现在能够向栈添加数据了,同时还需要从中再次获取,这也是相当简单的

1 读取头节点的当前值
2 读取head->next
3 将head设置为head->next
4 返回获取的数据
5 删除获取的节点

然而在多线程的情况下并不简单。如果有两个线程同时从栈中移除项,它们可能在1时同时读取到了头节点的值,如果一个线程在执行1-5的步骤时,另一个线程b执行了2,这时线程b就关联了一个悬挂指针。这是写lock-free代码时一个最大的问题,这时你可以不去做5

但是这不能解决所有的问题,还有另一个问题,如果两个线程读取了相同的head,就会返回相同的节点,这就违反了栈数据结构的意图,所以需要避免这种情况。可以使用解决push问题时相同的办法,使用CAS更新head节点无论是push一个新节点还是另一个线程pop这个节点时,发生CAS失败都需要返回到1,一旦CAS调用成功,表明只有一个线程pop了节点,就能够安全的执行4

template<typename T>
class lock_free_stack {
public:
void pop(T& result) {
node* old_head=head.load();
while(!head.compare_exchange_weak(old_head,old_head->next));
result=old_head->data;
}
};

尽管这是简约和不错的,还是有一些问题。首先,无法在一个空链表上执行,如果head是一个空指针,在读取next时就会导致一个无法定义的行为。通过在while循环中检测或者在空栈上抛出异常或者返回一个指示标识很容易修正


第二个是异常安全问题,线程安全的栈中,如果是复制值时发生异常,这个值会丢失,在这种情况下,通过对结果的引用是一个可接受的方案,因为能够保证堆栈在抛出异常时保证不变。不幸的是,在这里不能这么去做,只能保证队列里的一个节点只会被一个线程取走(多线程情况下,一个节点可能会被多个线程同时取走)然后做安全复制,
这意味着这个节点已经从队列中移除。因此通过引用传递返回值不再是优点,如果想安全返回值,可以返回指向数据的智能指针。


如果返回智能指针,返回nullptr则意味着没有值返回,但是这需要这个数据在堆上分配过。如果这个堆分配作为pop的一部分,如果堆分配失败会导致异常,不过可以在push的时候就分配内存。
返回一个std::shared_ptr<> 不会抛出异常,所以pop也是安全的

template<typename T>
class lock_free_stack {
private:
struct node {
std::shared_ptr<T> data; 1
node* next;
node(T const& data_):
data(std::make_shared<T>(data_)){} 2
};
std::atomic<node*> head;
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));
}
std::shared_ptr<T> pop(){
node* old_head=head.load();
while(old_head && 3
!head.compare_exchange_weak(old_head,old_head->next));
return old_head ? old_head->data : std::shared_ptr<T>(); 4
}
};


数据保存在指针中 1
在构造函数里必须要为数据在堆上分配内存 2
在使用compare_exchange_weak循环中引用old_head前必须判断是否是nullptr指针。尽管这是lock-free,但不是wait-free,因为如果compare_exchange_weak
保持错误, 在push和pop中的while循环理论上是无限循环。如果是c#或者java有gc存在,基本就结束了,对于c++而言还需要回收内存。
如果想释放一个节点时需要确保没有其他线程还拥有这个指针。如果仅仅是一个线程在特定的栈上调用pop很简单,一旦添加到栈push就不再接触节点,所以调用pop的线程
就是唯一能够接触到节点的线程,可以安全删除节点。

另一方面,如果需要处理多个线程在一个栈实例上调用pop(),检查从pop中访问的节点


如果没有线程pop(),删除所有节点等待删除目前很安全。因此,如果在提取数据时将节点添加到要删除的列表中,那么当没有线程调用时,可以删除它们所有的节点


如果在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
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;
}
};


原子变量threads_in_pop用于当前使用pop的线程个数 1
在pop的开始增加计数 2 ,在try_reclaim内部减少计数节点被删除时调用。由于潜在的延迟了删除节点,使用swap 删除节点而不是复制指针,所以这个节点能够被自动删除
当不再需要的时候而不是一直保存


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
node* nodes_to_delete=to_be_deleted.exchange(nullptr); 2
if(!--threads_in_pop) { 3
delete_nodes(nodes_to_delete); 4
} else if(nodes_to_delete) { 5
chain_pending_nodes(nodes_to_delete); 6
}
delete old_head; 7
} 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
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,first));
}

void chain_pending_node(node* n) {
chain_pending_nodes(n,n); 12
}
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值