【问题标题】:Using Bcl ImmutableDictionary in private field在私有字段中使用 Bcl ImmutableDictionary
【发布时间】:2013-01-17 07:02:53
【问题描述】:

假设我有一个将从多个线程调用的类,并将在该类的私有字段中的 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增加了价值,但是因为使用了相同的引用,它已经被添加了?

【问题讨论】:

  • 是的,我相信你描述的方式不是线程安全的。您可能需要自己锁定它。
  • 有多个线程尝试插入同一个项目的情况是什么?如果要并行化工作,则需要跨线程/机器对数据进行分区。
  • 如果分配是_dict[key] = value; - 甚至可能删除ContainsKey 检查 - 那将是线程安全的(?)

标签: c# multithreading thread-safety immutability base-class-library


【解决方案1】:

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

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

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】:

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

    还有一个问题:同时写入不同的键只会丢失写入

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

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

    【讨论】:

    • 不同的是,使用Dictionary,你不能使用CAS,你必须使用锁,这可以有所作为。
    • @svick 严格来说,您可以将 CAS 与 Dictionary 一起使用...但这是在吹毛求疵,您是对的。
    • 我设法通过将字典复制到局部变量、执行函数然后将结果分配回私有字段来绕过重复键,但是您提出了一个很好的观点,即并发写入不同的密钥会丢失。我想在这种情况下,某种 ImmutableSet 可能是更好的选择,然后在本地复制并合并结果。
    • @chrisortman 它不是通过联合操作完成的,因为删除可能会被覆盖。这是分布式计算/状态的一个基本问题。您不能合并任意更改。
    • ConcurrentDictionary 具有极差的性能特征(具有讽刺意味的是),但在极其并发的操作中除外。
    【解决方案3】:

    现在 BCL 中有 a class available 来执行相同的 CAS 循环。这些与 Andrew Arnott 的答案中的扩展方法非常相似。

    代码如下所示:

    ImmutableInterlocked.AddOrUpdate(ref _dict, key, value, (k, v) => v);
    

    【讨论】:

      【解决方案4】:

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

      【讨论】:

        猜你喜欢
        • 2020-01-02
        • 2020-04-25
        • 2015-08-12
        • 2014-02-24
        • 1970-01-01
        • 1970-01-01
        • 2014-06-19
        • 2013-01-29
        • 2013-03-08
        相关资源
        最近更新 更多