ETS 奇特之处

2019 年 1 月 7 日 · 作者:Lukas Larsson

在实现新的可扩展的 ordered_set时,我们遇到了一个奇怪的问题,即在并行插入元素时,迭代表时的保证。

场景:#

> Tab = ets:new(test_table,
                [set, public, {write_concurrency, true}]).
#Ref<0.1705802953.985792516.98626>
> P1 = spawn(fun() ->
               ets:insert(Tab, {fir, 1}),
               ets:insert(Tab, {sec, 2})
             end).
> K1 = ets:first(Tab), K2 = ets:next(Tab, K1).

K1K2 的理论可能值是什么?让我们首先列出显而易见的情况

  • K1 = fir, K2 = sec - 两个值都已插入并在术语顺序中找到
  • K1 = sec, K2 = fir - 由于这是一个 set,哈希算法可能会将 sec 放在 fir 之前
  • K1 = fir, K2 = '$end_of_table' - 只有 fir 有时间被插入
  • K1 = '$end_of_table', K2 = badarg - 没有插入任何元素

然而,也可能出现

  • K1 = sec, K2 = '$end_of_table'

起初,这对我来说非常违反直觉。ets:first/1 如何找到插入的第二个值,但在迭代时却没有找到之前插入的值?

答案可以在 write_concurrency 功能的实现方式中找到。想象一下我们有一个哈希表,其中每个桶都由互斥锁保护。当插入一个新元素时,必须获取当前桶的互斥锁,当迭代哈希表时,我们会依次获取我们迭代的桶的每个互斥锁。

初始表:#

桶 #
1 []
2 []
3 []
4 []

完成的表:#

桶 #
1 [{fir,1}]
2 []
3 []
4 [{sec,2}]

因此,在导致奇怪行为的场景中,将发生以下情况

  • 当表为空时调用 ets:first/1 并迭代到桶 #2。
桶 #
1 []
2 (first) []
3 []
4 []
  • 操作系统进行上下文切换,允许 P1 运行。
  • P1 插入 {fir,1}{sec,2},然后退出。
桶 #
1 [{fir,a}]
2 (first) []
3 []
4 [{sec,b}]
  • ets:first/1 调用恢复,只会看到 sec,然后是 '$end_of_table'

这样说出来,只获取作为第二个元素插入的元素就更符合逻辑了。对于类型为 set 的表来说,这通常不是问题,因为 set 的迭代顺序是任意的,您无论如何都不能依赖它。

然而,对于 ordered_set,您很可能依赖于定义的迭代顺序,并期望 ets:first/1 返回一个在某个时间点至少在表中最前面的键。但由于与 set 相同的原因,如果需要这种保证,则不能保证,您必须要么不使用 write_concurrency,要么找到其他同步方法,要么依赖运气……这些竞争非常罕见,但在大量使用的表中,它们最终会发生。

同样的奇怪之处也适用于各种表迭代;ets:next/1ets:select/1-3ets:match/1-3 和其他类似方法。它们都可能会错过并发插入的键,并返回一个从未存在于表中紧接在先前返回的键之后的键。