在私有领域中使用Bcl ImmutableDictionary

chr*_*man 5 c# multithreading immutability thread-safety base-class-library

假设我有一个将从多个线程调用的类,并且我将在此类的私有字段中将一些数据存储在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);
       }
    }
}
Run Code Online (Sandbox Code Playgroud)

这可以通过多个线程以这种方式调用,你会得到关于字典中已存在的密钥的错误吗?

Thread1检查字典看到false Thread2检查字典看到false Thread1添加值并引用_dict更新Thread2添加值,但它已经添加,因为它使用相同的引用?

And*_*ott 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

事实上,我经常使用这种模式,为此我定义了一个静态方法:

/// <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;
}
Run Code Online (Sandbox Code Playgroud)

然后您的 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));
    }
}
Run Code Online (Sandbox Code Playgroud)


usr*_*usr 4

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

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

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

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