【问题标题】:Immutable data structures and concurrency不可变数据结构和并发
【发布时间】:2016-07-30 00:06:00
【问题描述】:

我试图了解如何在并发编程中使用不可变数据结构来消除对锁定的需求。我在网上读过一些东西,但还没有看到任何具体的例子。

例如,假设我们有一些代码 (C#) 在 Dictionary< string, object> 周围使用锁:

class Cache
{
    private readonly Dictionary<string, object> _cache = new Dictionary<string, object>();
    private readonly object _lock = new object();

    object Get(string key, Func<object> expensiveFn)
    {
        if (!_cache.ContainsKey("key"))
        {
            lock (_lock)
            {
                if (!_cache.ContainsKey("key"))
                    _cache["key"] = expensiveFn();
            }
        }
        return _cache["key"];
    }
}

如果_cache 是不可变的,那会是什么样子?是否可以删除lock 并确保expensiveFn 不会被多次调用?

【问题讨论】:

  • 你为什么不只使用ConcurrentDictionary
  • 如何防止昂贵的Fn被调用两次?
  • 因为ConcurrentDictionary 已经有一个方法可以完全按照您的方法为Dictionary 所做的工作,除了它由MS 有效地调整,并且具有支持更高效的多线程访问的底层存储结构。跨度>
  • 我试图了解使用不可变数据结构如何简化并发编程并消除对锁的需求。不确定 ConcurrentDictionary 对此有何帮助
  • @Servy 我认为他的观点是,这段特定的代码不是解决现实世界问题的一部分,他只是想了解使用不可变数据结构如何改变你做并发。使用 ConcurrentDictionary 是解决现实世界问题的完美解决方案,但它无助于他对这个特定问题的理解。

标签: c# concurrency immutability


【解决方案1】:

简短的回答是它没有,至少不是完全没有。

不变性仅保证另一个线程在您使用它时无法修改您的数据结构的内容。一旦你有了一个实例,这个实例就永远不能被修改,所以你总是可以安全地阅读它。任何编辑都需要创建实例的副本,但这些副本不会直接影响已引用的任何实例。

在多线程应用程序中需要锁定和同步结构的原因仍然很多,即使是不可变对象也是如此。它们主要处理与时间相关的问题,例如竞争条件或控制线程流以便活动在正确的时间发生。不可变对象不会真正帮助解决这类问题。

不变性使多线程更容易,但它并不容易


就您关于不可变字典的外观的问题而言。我不得不说,在大多数情况下,在您的示例中,甚至使用不可变字典都没有多大意义。因为它被用作“活动”对象,随着项目的添加和删除而固有地发生变化。即使在围绕不变性设计的语言(如 F#)中,也存在用于此目的的可变对象。有关详细信息,请参阅this link。不可变版本可以在here找到。

【讨论】:

  • 确实,不可变数据结构并不能消除对锁的需求。但我想补充一点,不变性有时使您能够用 CAS(Compare-and-swap,请参阅Interlocked.CompareExchange)操作之类的原子操作替换锁。 Microsoft 的不可变集合包含一个 ImmutableInterlocked 类,该类具有使用 CAS 操作“修改”不可变列表、集合、字典等的操作。
【解决方案2】:

不可变数据结构背后的基本思想是减少(注意我说的是“减少”,而不是“消除”)并发锁定的需要,每个线程要么在本地副本上工作,要么针对不可变数据结构工作,所以有不需要锁定(没有线程可以修改任何其他线程的数据,只能修改他们自己的数据)。仅当多个线程可以同时修改相同的可变状态时才需要锁定,否则您可能会出现“脏读”和其他类似问题。

【讨论】:

  • 这并不完全正确,并不是每个线程都有自己的副本。事实上,您可以保证每个人都在使用的共享副本永远不会被修改,如果有人想要修改它,他们需要创建一个新对象并且将不再使用共享副本。
  • 很公平,这更准确。我相应地编辑了我的帖子。
【解决方案3】:

为什么不可变数据很重要的一个例子: 假设您有一个由两个不同线程访问的人员对象。 如果线程 1 将人保存到地图中(人哈希包含人名),那么另一个线程 2 会更改人名。 现在线程 1 将无法在地图中找到此人,而它实际上就在那里!

如果 person 是不可变的,则不同线程持有的引用将不同,即使 user2 更改了他的名字,thread1 也能够在 map 中找到该人(因为将创建一个新的 person 实例)。

【讨论】:

    猜你喜欢
    • 2015-04-27
    • 1970-01-01
    • 1970-01-01
    • 2014-10-14
    • 2012-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多