不可变数据结构和并发

Immutable data structures and concurrency

我正在尝试了解如何在并发编程中使用不可变数据结构来避免对锁定的需要。我已经在网上阅读了一些内容,但还没有看到任何具体示例。

例如,假设我们有一些代码 (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 不会被多次调用?

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

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

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

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


就你关于不可变字典的问题而言。我不得不说,在大多数情况下,在您的示例中,甚至使用不可变字典也没有多大意义。因为它被用作 "active" 对象,随着项目的添加和删除而固有地改变。即使在围绕不变性设计的语言中,如 F#,也有用于此目的的可变对象。参见 this link for more details. The immutable versions can be found here

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

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

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