crossbeam-epoch
crossbeam-epoch 是 crossbeam 用来管理内存的子包,它实现了 epoch-based reclaim。并发编程的时候会有内存回收问题和 ABA 问题,但本质上,解决了内存问题,就解决了 ABA 问题,因为后者本质是内存 reuse 导致的。
Epoch Based Reclaim 有点类似某种程度上的 RC,但是它的 RC 的粒度要粗很多:针对 Epoch,而非针对 Object,这让维护和使用它的代价变小了很多,它引入的语义如下:
There are a few non-GC-based ways of managing memory for lock-free code, but they all come down to the same core observations:
- There are two sources of reachability at play – the data structure, and the snapshots in threads accessing it. Before we delete a node, we need to know that it cannot be reached in either of these ways.
- Once a node has been unlinked from the data structure, no new snapshots reaching it will be created.
综上,一个对象只能被删除一次,随着最后一个可能可能读到它的线程结束,它就是「可清除」的了,它会随着 epoch 的推高而清除。
crossbeam-epoch 实现了上述的语义,并引入了一套子系统,来表示对应的内存指针和引用。
Epoch 提供了:
- Global Epoch Counter (取值 0/1/2, 三个 epoch 迭代)
- 每个 Epoch 挂的 Garbage
- 每个 Thread 是否是 “Active” 的
- 每个 Thread 的 Epoch
每个线程启动的时候,会把自己的 Epoch 设置成全局的 Epoch,unlink 一个对象的时候,会放到 Global Garbage 列表中,「当前 Epoch」对应的地方。线程完成操作的时候,会清除 Active 标记。这里有3个 epoch,没有任何读者的 Epoch 理论上是 current - 2, 它上面的对象可以被 GC。
性能相关可见:Performance of memory reclamation for lockless synchronization
API of crossbeam-epoch
pin() 产生一个本线程的 Guard ,Guard 没退出表示这个线程还在活跃,实际上就是 某个线程/版本的 RAII。
然后有下面的智能指针:
To put the
Guardto use, Crossbeam provides a set of three pointer types meant to work together:
Owned<T>, akin toBox<T>, which points to uniquely-owned data that has not yet been published in a concurrent data structure.Shared<'a, T>, akin to&'a T, which points to shared data that may or may not be reachable from a data structure, but it guaranteed not to be freed during lifetime'a.Atomic<T>, akin tostd::sync::atomic::AtomicPtr, which provides atomic updates to a pointer using theOwnedandSharedtypes, and connects them to aGuard.
上面的类型足以表述了,具体见:https://aturon.github.io/blog/2015/08/27/epoch/
一些要点是(在上文的 Managing garbage 段):
- unlink 的时候可以运行 destructor, 而 ebr 只具体回收内存(安全性:操作都会走 cas)
- 线程有一些 TLS 的垃圾列表,可能会在有一定阈值的时候 emit 到全局的列表中
epoch::pin的时候,可能会 emit 垃圾甚至触发垃圾清理
Code
Tools
1 | ➜ src git:(master) tree |
首先,我们关注一下周边工具,例子是 sync 目录,这个目录有点循环引用的味道,用 crossbeam-epoch 实现了两个组件:
- 并发的 linked-list queue:按照 http://dl.acm.org/citation.cfm?id=248106 和 https://doi.org/10.1007/978-3-540-30232-2_7 实现
- 并发的侵入式链表:http://www.cs.tau.ac.il/~afek/p73-Lock-Free-HashTbls-michael.pdf
上面两个 case 可以当作实现的例子,然后 crossbeam-epoch 也用到了它俩。
Queue 的接口比较简单:
1 | impl<T> Queue<T> { |
而 List 是一个侵入式结构,大概需要实现一个 IsElement:
1 | /// An entry in a linked list. |
这个结构就,非常侵入式,牛逼的。
Epoch
然后是 epoch 有关的类型,下面这部分在 epoch.rs:
1 | //! The global epoch |
看上面的表示,Epoch 最后一位表示是否 pin,这个在全局 Epoch 里面是没有意义的,但是局部数据对象可能依赖这个数据,而前面的数据代表具体版本:
1 | /// An epoch that can be marked as pinned or unpinned. |
这里还包了一层 AtomicEpoch, AtomicEpoch 提供了对 Epoch 的 load, store 和 cas 操作:
1 | /// An atomic value that holds an `Epoch`. |
Deferred
Deferred 是一个 defer 的 data + destructor, 我感觉可以 Box<Fn(...)>,不过它这感觉泛用很多。它靠谱的功能如下:
1 | /// Number of words a piece of `Data` can hold. |
其实很明显了,感觉还是挺简单的。
Bag
在 internal.rs 里面,实现了 Bag 和 SealedBag. Bag 是一组 Deferred, 而 SealedBag 则是定了某个版本的 SealedBag, 他们内容如下:
1 | //! # Thread-local bag |
代码:
1 | /// Maximum number of objects a bag can contain. |
外部接口
外部很多接口是现在 default.rs 里面。很多读者可能会很困惑,刚刚不是还在讲工具类吗,现在怎么外部接口了,答案是剩下内容都是紧密缝合的,适合自顶向下讲了。工具先看完,然后自顶向下推进,还挺好的。
https://github.com/crossbeam-rs/crossbeam/blob/master/crossbeam-epoch/src/default.rs
这里有:
- 全局 lazy_static 的
Collector - 单个线程一个的
LocalHandle, 由COLLECTOR.register()生成
然后外部的 pin 接口会拿到 tls 的 LocalHandle, 用它来 pin.
而 Collector 和 LocalHandle 则是 内部的 Global 和 Local 的包装器:
1 | /// An epoch-based garbage collector. |
这俩哥们基本属于套娃包装,所以我们最后当然必须再来看看 internal.rs,这里面东西主要有 Global 和 Local:
Global 是全局状态池子,由 Collector 包装(等下会讲),内容大概如下:
1 | /// The global data for a garbage collector. |
Global 会挂着:
SealedBag的队列,作为延迟调用的函数List<Local>,作为活跃的线程的侵入式链表epoch, 全局的 atomic epoch,最后一位是无用的
Global 的方法得全贴出来:
1 | impl Global { |
这里,其他函数含义都非常清晰,难一点的是 try_advance ,如果它发现现在所有活跃线程都是 pinned 的,且等于自身周期,那么它会推进。这个地方有点难懂,我们可以回顾一下,可以推进是因为只有这个版本的 reader 了,再之前的数据对象可以 gc 掉,这里和 pin 的流程是有关的。总之,每个 Local 都是本周期且 pin 了,就可以推进 + 回收了。回收流程见 collect
至于 Local,其实挺…听不 RAII 的,它会在 register 的时候 创建,pin 的时候初始化,release_handle 的时候销毁。它本身会被 TLS 的使用,和线程绑定:
1 | /// Participant for garbage collection. |
你会发现它也有个 epoch, 没错,这玩意只有 pin 的时候会初始化。我看还有 repin 什么的,估计是为了优化准备的吧。
最后有个 Guard,本身就是很简单的绑定 Local 的组件了。需要注意的是,Local 和 Guard 大部分接口都是串行的,只有读 epoch 的时候,会并行,它也只靠 epoch 和 collector 与 Global 互相通信