6

假设我有一个将从多个线程调用的类,并且将在该类的私有字段中的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 检查字典是否为假 Thread2 检查字典是否为假 Thread1 添加值并更新对 _dict 的引用 Thread2 添加值,但它已添加,因为它使用相同的引用?

4

4 回答 4

5

在使用不可变字典时,您绝对可以是线程安全的。数据结构本身是完全线程安全的,但是您在多线程环境中对其应用更改必须仔细编写以避免在您自己的代码中丢失数据。

这是我经常用于这种情况的一种模式。它不需要锁,因为我们所做的唯一突变是单个内存分配。如果必须设置多个字段,则需要使用锁。

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));
    }
}
于 2013-02-07T15:58:07.990 回答
4

的,与往常一样适用相同的比赛(两个线程都读取,什么也没找到,然后两个线程都写入)。线程安全不是数据结构的属性,而是整个系统的属性。

还有另一个问题:对不同键的并发写入只会丢失 writes

你需要的是一个ConcurrentDictionary. 如果没有额外的锁或 CAS 循环,您无法使用不可变的方法来实现这一点。

更新:这些评论让我相信,ImmutableDictionary如果写入不频繁,使用 CAS 循环进行写入实际上是一个非常好的主意。读取性能将非常好,并且写入与同步数据结构一样便宜。

于 2013-02-03T10:11:20.667 回答
2

现在 BCL 中有一个类可用于执行相同的 CAS 循环。这些与 Andrew Arnott 回答中的扩展方法非常相似。

代码如下所示:

ImmutableInterlocked.AddOrUpdate(ref _dict, key, value, (k, v) => v);
于 2018-06-01T15:00:35.560 回答
1

访问实例变量会使 Add() 方法不可重入。复制/重新分配给实例变量不会改变不可重入性(它仍然容易出现竞争条件)。在这种情况下,ConcurrentDictionary 将允许访问没有完全一致性,但也没有锁定。如果需要跨线程 100% 的一致性(不太可能),那么字典上的某种锁定是必要的。了解可见性范围是两个不同的东西非常重要。实例变量是否是私有的与其作用域无关,因此也与其线程安全无关。

于 2013-02-05T17:10:54.530 回答