问题描述
我一直在关注由 Simon Robinson 撰写的关于并发集合的 Pluralsight 课程。
他以下列方式使用 AddOrUpdate
以使其成为线程安全的:
public bool TrySellShirt(string code)
{
bool success = false;
_stock.AddOrUpdate(code,(itemname) => { success = false; return 0; },(itemName,oldValue) =>
{
if (oldValue == 0)
{
success = false;
return 0;
}
else
{
success = true;
return oldValue - 1;
}
});
if (success)
Interlocked.Increment(ref _totalQuantitySold);
return success;
}
所以,我知道 AddOrUpdate 并不完全是原子的,正如它在文档中所说:“addValueFactory 和 updateValueFactory 委托在锁外调用,以避免在下执行未知代码时可能出现的问题一把锁。 "
这对我来说很清楚。不清楚的是在委托中将 success
设置为 false
有什么意义。 AddValueFactory
参数被故意用作 lambda,因此可以设置 success = false
,而不是仅仅返回 0。我有点理解/认为如果方法/lambda 被另一个线程中断(并且它可以被中断,因为它在锁外调用),它将尝试重复自身,因此我们应该将任何相应值的状态设置为其初始值以干净地进行新的迭代,因此设置 success = false;
。
同样来自文档:如果您在不同的线程上同时调用 AddOrUpdate,addValueFactory 可能会被多次调用,但它的键/值对可能不会在每次调用时都添加到字典中。 >
如果是这样,我一直在 source.dot.net 上检查 AddOrUpdate
的源代码,我看不到任何地方使用任何锁,我可以看到 { {1}} 和 TryAddInternal
。
无论如何,前面的方法有效,但我不明白它为什么有效,一旦我删除了看似不必要的 TryUpdateInternal
赋值,它就不起作用,就是不匹配。所以我很好奇是什么让这些代表在失败后重蹈覆辙?
我的问题是:
1。如图所示使用 success = false
是否安全,还是我应该锁定所有内容并忘记它?
2。是什么让代表在被打断后重复自己?它与“比较和交换”有什么关系吗? (对这个最好奇);
3。是否有任何主题/概念让我查看以更好地了解线程安全环境?
解决方法
因为 addValueFactory
和 updateValueFactory
委托由 ConcurrentDictionary 调用而没有任何锁定,所以在 add/updateValueFactory 代码运行时另一个线程可能会更改字典的内容。为了处理这种情况,如果调用了 addValueFactory
(因为该键在字典中不存在),它只会在键 still 不存在于词典。同样,如果调用了 updateValueFactory
,它只会在当前值仍然是 oldValue
时更新键的值。
如果在 add/updateValueFactory 代码运行时由于另一个线程添加/更新/删除相同的键而导致不匹配,它将简单地尝试根据字典的最新内容再次调用适当的委托(委托没有被“中断”,并且是字典本身再次调用它们,添加/更新的键的值已经改变)。这解释了为什么即使 success = false
被初始化为 false,您仍然需要在 lambdas 中进行 success
赋值。以下示例可能有助于可视化行为:
初始字典状态:_stock["X"] = 1
步骤 | 主题 1 | 主题 2 |
---|---|---|
1 | 调用 _stock.AddOrUpdate("X",...)
|
|
2 |
updateValueFactory 已调用 (oldValue = 1) |
|
3 | 调用 _stock.AddOrUpdate("X",...)
|
|
4 |
updateValueFactory 已调用 (oldValue = 1) |
|
5 | 设置 success = true ,返回 oldValue - 1 = 0 |
|
6 | 字典检查键 "X" 的值是否仍然 = 1 (true) | |
7 | 键“X”的值更新为 0
|
|
8 | 设置 success = true ,返回 oldValue - 1 = 0 |
|
9 | 字典检查键“X”的值是否仍为 = 1 (false) | |
10 |
updateValueFactory 再次调用 (oldValue = 0) |
|
11 | 设置 success = false ,返回 0 |
|
12 | 字典检查键 "X" 的值是否仍然 = 0 (true) | |
13 | 键“X”的值更新为 0
|
|
14 |
success 的最终值为 false
|
success 的最终值为 true
|
请注意,如果没有在 success = false
的 oldValue == 0
分支中明确设置 if
,线程 1 会认为它仍然成功销售了该项目,即使有不再有库存,因为线程 2 卖掉了最后一个。
因此,您问题中的技术按预期工作。