文/玄魂
从 .NET4.0开始,到现在的4.5,我们可以感受得到微软在并行、多线程、异步编程上带给开发人员的惊喜。在多线程开发中,无可避免的涉及多个线程共享对象问题,Immutable Object(不可变对象)在保证线程安全方面的重要性被凸显出来。简单不可变对象,比如单例,我们可以很轻松的创建并维护,一些复杂对象,对象引用或者集合对象的场景 ,创建和维护不可变对象变得困难了很多。微软在这方面也做了很多努力,目前看最令我欣喜的就是Immutable Collections了。如果您了解函数式编程,那么对此肯定不会陌生。
当然除了线程安全,不可变集合还有其他的应用场景,本文也会有所涉及。
笔者最近研读了几篇MSDN Blog中关于Immutable Collections的英文博文(在文后会给出链接)。我看到的博客中的代码和我下载的版本有些出入,我根据自己的理解重新整理,改编成此文,水平有限,欢迎讨论。
这类对象只能在编译时赋值,在C#中const类型的变量属于这个类型。
运行时初始化一次,之后再也不会被改变。典型的单例对象就属于这一类。
以C#为例,对象本身是Static ReadOnly类型,但是这不能保证该对象内部成员是线程安全的,这类对象具有浅度不变性,如果能保证对象本身、对象内部任何成员或者嵌套成员都具有不变性则该对象具有深度不变性。
显然,具有深度不变性的对象是理想的线程安全模型。
不要误会安装的含义,这里是指从Nuget安装提供不可变集合功能的Dll。
运行环境:vs2012,.NET 4.5
这个Preview版本的安装包包含了如下不可变类型:
· ImmutableStack<T>
· ImmutableQueue<T>
· ImmutableList<T>
· ImmutableHashSet<T>
· ImmutableSortedSet<T>
· ImmutableDictionary<K, V>
· ImmutableSortedDictionary<K, V>
每种类型都继承自相应的接口,从而保证之后不可变类型的可扩展性。
先以ImmutableList<T>为例,开始我们的不可变集合之旅。
注意上面的代码,我们没有使用构造函数来初始化ImmutableList<string>集合,而是使用名为ImmutableList的Create方法,该方法返回一个空的不可变集合。使用空集合在某些情况下可以避免内存浪费。
Create方法有7个重载,可以传入初始化数据和比较器。
下面我们尝试向这个集合中添加一些数据。
我想您已经看到Immutable Collections和传统集合的一个区别 了,Add方法创建了一个新的集合。这里我们也可以使用AddRange方法批量添加数据创建新的实例。
有时,我们可能更需要对一个集合多次修改才能到达要求。从上面的示例我们知道,每次修改都会创建新的集合,这就意味着要开辟新的内存,并且存在数据拷贝。程序本身的执行效率会下降同时GC压力会增大。
其实同样的问题再String类型上也存在,反复的修改字符串值存在同样的问题,.NET中StringBuilder用来解决这个问题。类似的IMMUTABLE COLLECTIONS也提供了Builder类型。同时我们具有了在迭代的同时修改集合的能力!
下面我们来看看Builder的基本应用:
在上面的代码中,使用ToBuilder方法获取Builder对象,在最后使用To ToImmutable方法返回IMMUTABLE COLLECTION。这里需要注意的是ToBuilder方法并没有拷贝资源给新Builder对象,Builder的所有操作都和集合共享内存。也许您要怀疑,既然是共享内存,那么Builder修改数据的时候集合怎么能不变化呢?这是因为ImmutableList的内部数据结构是树,只需要在更新集合的时候创建一个新的引用包含不同节点的引用即可。内部的实现原理,我会在下一篇博文中继续探讨。上面的代码既没有修改原来的IMMUTABLE COLLECTION也没有拷贝整个集合的内部操作。运行结果如下:
immutable collections 在很多方面,性能优于可变集合。当然性能上的优势和可变还是不可变的关系并不大,主要原因在于immutable collections内部的数据结构。比如下面的代码:
每次访问都会引起内存拷贝的操作。
但是如果使用immutable collection就可以避免这个问题:
内部原理和对性能的影响放在下一篇博客探讨,下面的列表是在算法复杂度层面的对比:
Mutable (amortized) |
Mutable (worst case) |
Immutable |
|
Stack.Push |
O(1) |
O(n) |
O(1) |
Queue.Enqueue |
O(1) |
O(n) |
O(1) |
List.Add |
O(1) |
O(n) |
O(log n) |
HashSet.Add |
O(1) |
O(n) |
O(log n) |
SortedSet.Add |
O(log n) |
O(n) |
O(log n) |
Dictionary.Add |
O(1) |
O(n) |
O(log n) |
SortedDictionary.Add |
O(log n) |
O(n log n) |
O(log n) |
在内存使用方面,Immutable Collections要比可变类型的集合要多,空间换时间,这个世界上没有两全其美的事情。
本篇博文只是浅尝则止,从概念上为您介绍了Immutable Collections的基本定义,简单应用。
我并没有拿典型的应用场景来举例,但是您可以从它们的线程安全,性能,内存使用等特性上权衡使用。如果有机会,我会将我的实际应用场景分享给您。
接下来,在下一篇博客中,我会探讨Immutable Collections的内部原理。也许你现在不会使用.NET4.5,但是其内部原理却是和平台无关的。
参考资料: