c++ 多线程 垃圾回收器_C++垃圾回收器:Wait Free Queue

这篇文章介绍一下gc_ptr中用到的一些无锁容器。不同于用CAS旋转锁实现的无锁容器,作者实现的容器是Wait Free的,永远不会阻塞。先来看看Queue,Queue由于FIFO的特点,对Queue的操作都在Queue的两端进行,非常适合进行并行处理。网上也有不少无锁Queue的实现,但不少都是基于CAS旋转锁,链表实现的,或者使用环形数组,固定长度实现。总感觉不是很完美,于是作者自己写了一个Queue,目前这个Queue是玩具级的实现,主要是为了实践一下作者的构想与思路。好的,接下来看看作者实现的Queue有哪些特点:

  • Wait Free级别的无锁
  • 泛型容器
  • 支持多生产多消费
  • 环形数组实现,支持自动扩容。

在来看看这个Queue有哪些成员变量,函数:

c0697d120cd0e51a4e8acfec99ee35f7.png

现在具体解释一下,各个成员的作用:

  • m_queue:队列主体,所有数据存在这里。
  • m_states:状态数组,由一组atomic组成。负责记录m_queue在每个位置的状态,一共由三种状态:
  1. free(表示m_queue在此位置为空,没有数据)
  2. vaild(表示m_queue在此位置有数据)
  3. copying(表示m_queue在此位置正在进行enqueue或者dequeue,正在拷贝数据)
m_enqueue_count:表示调用了多少次enqueue,用来计算入列时写入的位置和索引。m_dequeue_count:表示调用了多少次dequeue,用来计算出列时读取的位置和索引。m_size:表示m_queue中有多少数据m_capacity:表示m_queue的最大长度。m_enqueue:表示enqueue的调用情况,开始调用enqueue时会加一,调用结束时减一。m_dequeue:表示dequeue的调用情况,开始调用dequeue时会加一,调用结束时减一。m_offset:配合m_enqueue_count,m_dequeue_count,用来计算入列出列的位置。m_resize: 当Queue满时,m_resize置true,阻塞enqueue,dequeue。并开始进行resize扩容,结束后m_resize置false。

wait_free_queue是如何做到Wait Free的?来看Enqueue函数

6eaadf2c012c86e8e1903f1029e8653d.png

上图的enqueue是作者为了讲解,去除了resize机制的版本,项目中的enqueue比这个还要复杂一点。注意,上面用到的CAS函数不是CAS自旋锁,是为了保证变量的正确性使用的。例如m_size,由于m_size存在上限(m_capacity),不能简单的调用fetch_add,不然会超出上限,需要使用CAS来保证正确性。

do

如上图当m_size达到最大值后便不会再增加,并且会进入无限循坏,但项目中的enqueue是有resize扩容机制的,不会卡在循环。

当一个线程调用enqueue时,首先增加m_size,抢占位置。只要容器没满,就可以继续走下去。这时由于m_size已经增加,大于零,所以其他线程也可以正确的调用dequeue,即使这时数据还没写入到位置(后面会有同步)。完成m_size增加后,线程会去抢占具体的写入位置:

do

注意实现中,写入位置是通过m_enqueue_count计算得来的。这么做是为了得到正确的数据的索引(编号),在多线程环境下,在容器外进行编号是没有意义的,因为第一个调用enqueue的线程可能是最后一个写入的,函数的执行顺序是不确定的。上面代码中的old_count就是数据的索引(编号),en_pos 既是写入位置。抢占完位置后,线程终于有机会去写入数据了,但还有最后一关:

while (!m_states[en_pos].compare_exchange_strong(before_state, wait_for_state::copying))
{
    before_state = wait_for_state::free;
}
m_queue[en_pos] = elem;
m_states[en_pos].compare_exchange_strong(after_state, wait_for_state::vaild);

由于容器中有没有数据是由m_size控制的,就算还没有写入数据,dequeue就已经可以正确运行了,所以dequeue可能会走在enqueue前面,这也是无锁编程中经典的ABA问题。于是这里多了一层状态屏障。一个数据元素有3种状态:free,copying,valid。

  • 当enqueue时元素的状态变化一定是free->copying->vaild。
  • 当dequeue时元素的状态变化一定是valid->copying->free。

通过状态屏障即保证了原子性,也解决了ABA问题。wait_free_queue在环形数组的任意位置,都可以有多组enqueue/dequeue并行。如果dequeue走在了enqueue的前面,dequeue会等到元素变成valid在运行。上面enqueue的是逻辑,dequeue的逻辑也是类似的。此外作者还实现了enqueue_range, dequeue_range来进行批量操作,并且保证批量操作时元素之间不被间断,是连续的。

如此三步走就可以实现一个Wait Free的队列:

  1. 抢占m_size来保证有写入/读取的位置。
  2. 抢占m_enqueue_count/m_dequeue_count来确定具体的写入/读取的位置。
  3. 通过状态屏障来保证写入/读取的原子性和避免ABA问题。

通过类似的技巧,还可以实现stack,buffer,等容器(基于哈希的set,map还有待验证)。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值