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).
K1
和 K2
的理论可能值是什么?让我们首先列出显而易见的情况
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/1
、ets:select/1-3
、ets:match/1-3
和其他类似方法。它们都可能会错过并发插入的键,并返回一个从未存在于表中紧接在先前返回的键之后的键。