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
Guard
to 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 theOwned
andShared
types, 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 互相通信