C# 不变的数据结构和并发性

C# 不变的数据结构和并发性,c#,concurrency,immutability,C#,Concurrency,Immutability,我试图理解在并发编程中使用不可变数据结构如何避免锁定的需要。我在网上读过一些东西,但还没有看到任何具体的例子 例如,假设我们有一些代码(C#),它在字典周围使用锁,这样做: class Cache { private readonly Dictionary<string, object> _cache = new Dictionary<string, object>(); private readonly object _lock = new object

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

例如,假设我们有一些代码(C#),它在
字典
周围使用锁,这样做:

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=new Dictionary();
私有只读对象_lock=新对象();
对象获取(字符串键,Func expensiveFn)
{
if(!\u cache.ContainsKey(“键”))
{
锁
{
if(!\u cache.ContainsKey(“键”))
_cache[“key”]=expensiveFn();
}
}
返回_缓存[“键”];
}
}

如果
\u cache
是不可变的,那会是什么样子?是否可以移除
,并确保不会多次调用
expensiveFn

简而言之,它不会,至少不会完全调用

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

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

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



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

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

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


如果person是不可变的,则不同线程持有的引用将不同,即使user2更改了person的名称,thread1也能够在映射中找到该person(因为将创建person的新实例).

为什么您不只是使用
ConcurrentDictionary
?这样做如何防止expensiveFn被调用两次?因为
ConcurrentDictionary
已经有了一个方法,它的功能与您的方法对
Dictionary
的功能完全相同,除非它是由MS高效调优的,并且有一个支持的底层存储结构更高效的多线程访问。我试图了解使用不可变数据结构如何简化并发编程并消除对锁的需要。不确定ConcurrentDictionary如何帮助that@Servy我认为他的观点是,这段特殊的代码并不是解决现实问题的方案的一部分,他只是想理解使用不可变数据结构如何改变并发方式背后的理论。使用ConcurrentDictionary是解决实际问题的完美有效的解决方案,但这无助于他对这一特定问题的理解。这不是完全正确的,不是每个线程都有自己的副本。事实上,您有一个guar保证每个人都在使用的共享副本永远不会被修改,如果有人想修改它,他们需要创建一个新对象,并且不再使用共享副本。公平地说,这更准确。我相应地编辑了我的文章。不可变的数据结构确实不会消除对锁的需要。但我想添加不可变的有时,您可以使用CAS(,请参阅)操作等原子操作替换锁。Microsoft的不可变集合包含一个
不可变互锁
类,该类的操作似乎通过使用CAS操作“修改”不可变列表、集合、字典等。