假设我有一个将从多个线程调用的类,并且我将在ImmutableDictionary中将一些数据存储在此类的私有字段中
public class Something {
private ImmutableDictionary<string,string> _dict;
public Something() {
_dict = ImmutableDictionary<string,string>.Empty;
}
public void Add(string key, string value) {
if(!_dict.ContainsKey(key)) {
_dict = _dict.Add(key,value);
}
}
}
这可以通过多个线程以这种方式调用,你会得到关于字典中已存在的密钥的错误吗?
Thread1检查字典看到false Thread2检查字典看到false Thread1增加了值,并且更新了对_dict的引用 Thread2增加了值,但它已经添加,因为它使用相同的引用?
答案 0 :(得分:4)
是,正常情况下应用相同的比赛(两个线程都读取,找不到任何内容,然后两个线程都写入)。 线程安全不是数据结构的属性,而是整个系统的属性。
还有另一个问题:并发写入不同的密钥只会丢失写入。
您需要的是ConcurrentDictionary
。如果没有额外的锁或CAS循环,你就无法使用不可变的那个。
更新:这些评论让我确信,如果写入很少,那么ImmutableDictionary
与CAS循环一起用于写入实际上是一个非常好的主意。读取性能非常好,写入与同步数据结构一样便宜。
答案 1 :(得分:4)
在使用不可变字典时,绝对可以保证线程安全。数据结构本身是完全线程安全的,但是您必须仔细编写在多线程环境中对其应用更改以避免在您自己的代码中丢失数据。
这是我经常用于这种情况的模式。它不需要锁,因为我们唯一的突变是单个内存分配。如果必须设置多个字段,则需要使用锁定。
using System.Threading;
public class Something {
private ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
public void Add(string key, string value) {
// It is important that the contents of this loop have no side-effects
// since they can be repeated when a race condition is detected.
do {
var original = _dict;
if (local.ContainsKey(key)) {
return;
}
var changed = original.Add(key,value);
// The while loop condition will try assigning the changed dictionary
// back to the field. If it hasn't changed by another thread in the
// meantime, we assign the field and break out of the loop. But if another
// thread won the race (by changing the field while we were in an
// iteration of this loop), we'll loop and try again.
} while (Interlocked.CompareExchange(ref this.dict, changed, original) != original);
}
}
事实上,我经常使用这种模式我为此目的定义了静态方法:
/// <summary>
/// Optimistically performs some value transformation based on some field and tries to apply it back to the field,
/// retrying as many times as necessary until no other thread is manipulating the same field.
/// </summary>
/// <typeparam name="T">The type of data.</typeparam>
/// <param name="hotLocation">The field that may be manipulated by multiple threads.</param>
/// <param name="applyChange">A function that receives the unchanged value and returns the changed value.</param>
public static bool ApplyChangeOptimistically<T>(ref T hotLocation, Func<T, T> applyChange) where T : class
{
Requires.NotNull(applyChange, "applyChange");
bool successful;
do
{
Thread.MemoryBarrier();
T oldValue = hotLocation;
T newValue = applyChange(oldValue);
if (Object.ReferenceEquals(oldValue, newValue))
{
// No change was actually required.
return false;
}
T actualOldValue = Interlocked.CompareExchange<T>(ref hotLocation, newValue, oldValue);
successful = Object.ReferenceEquals(oldValue, actualOldValue);
}
while (!successful);
Thread.MemoryBarrier();
return true;
}
然后,您的Add方法变得更加简单:
public class Something {
private ImmutableDictionary<string, string> dict = ImmutableDictionary<string, string>.Empty;
public void Add(string key, string value) {
ApplyChangeOptimistically(
ref this.dict,
d => d.ContainsKey(key) ? d : d.Add(key, value));
}
}
答案 2 :(得分:1)
访问实例变量使Add()方法不可重入。复制/重新分配给实例变量不会改变非重入(它仍然容易出现竞争条件)。 在这种情况下,ConcurrentDictionary将允许访问而没有完全一致性,但也没有锁定。如果跨线程需要100%的一致性(不太可能),则需要在Dictionary上进行某种锁定。 了解可见性和范围是两回事,这一点非常重要。 实例变量是否为私有变量与其范围无关,因此与其线程安全无关。
答案 3 :(得分:0)
现在BCL中有a class available来执行相同的CAS循环。这些与Andrew Arnott的答案非常相似。
代码如下所示:
ImmutableInterlocked.AddOrUpdate(ref _dict, key, value, (k, v) => v);