这篇文章介绍一下gc_ptr中用到的一些无锁容器。不同于用CAS旋转锁实现的无锁容器,作者实现的容器是Wait Free的,永远不会阻塞。先来看看Queue,Queue由于FIFO的特点,对Queue的操作都在Queue的两端进行,非常适合进行并行处理。网上也有不少无锁Queue的实现,但不少都是基于CAS旋转锁,链表实现的,或者使用环形数组,固定长度实现。总感觉不是很完美,于是作者自己写了一个Queue,目前这个Queue是玩具级的实现,主要是为了实践一下作者的构想与思路。好的,接下来看看作者实现的Queue有哪些特点:
- Wait Free级别的无锁
- 泛型容器
- 支持多生产多消费
- 环形数组实现,支持自动扩容。
在来看看这个Queue有哪些成员变量,函数:
![c0697d120cd0e51a4e8acfec99ee35f7.png](https://i-blog.csdnimg.cn/blog_migrate/7cc2fd5b68b08688468585e1312fc48d.jpeg)
现在具体解释一下,各个成员的作用:
- m_queue:队列主体,所有数据存在这里。
- m_states:状态数组,由一组atomic组成。负责记录m_queue在每个位置的状态,一共由三种状态:
- free(表示m_queue在此位置为空,没有数据)
- vaild(表示m_queue在此位置有数据)
- copying(表示m_queue在此位置正在进行enqueue或者dequeue,正在拷贝数据)
wait_free_queue是如何做到Wait Free的?来看Enqueue函数
![6eaadf2c012c86e8e1903f1029e8653d.png](https://i-blog.csdnimg.cn/blog_migrate/5bd3d4d70066035cc748b2c680542b56.jpeg)
上图的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的队列:
- 抢占m_size来保证有写入/读取的位置。
- 抢占m_enqueue_count/m_dequeue_count来确定具体的写入/读取的位置。
- 通过状态屏障来保证写入/读取的原子性和避免ABA问题。
通过类似的技巧,还可以实现stack,buffer,等容器(基于哈希的set,map还有待验证)。