不变性
简而言之,内存在初始化后不被修改时是不可变的。
用 C、Java 和 C# 等命令式语言编写的程序可以随意操作内存中的数据。物理内存区域一旦被留出,就可以在程序执行期间的任何时间由执行线程全部或部分修改。事实上,命令式语言鼓励这种编程方式。
以这种方式编写程序对于单线程应用程序来说非常成功。然而,随着现代应用程序开发向单个进程内的多个并发操作线程发展,引入了一个潜在问题和复杂性的世界。
当只有一个执行线程时,您可以想象这个单个线程“拥有”内存中的所有数据,因此可以随意操作它。但是,当涉及多个执行线程时,没有隐含的所有权概念。
相反,这个负担落在了程序员身上,他们必须竭尽全力确保内存中的结构对于所有读者来说都处于一致的状态。必须谨慎使用锁定结构,以防止一个线程在另一个线程更新数据时查看数据。如果没有这种协调,线程将不可避免地消耗刚刚更新到一半的数据。这种情况的结果是不可预测的,而且往往是灾难性的。此外,在代码中使锁定正确工作是出了名的困难,如果做得不好会削弱性能,或者在最坏的情况下,会出现死锁,无法恢复地停止执行。
使用不可变数据结构减轻了在代码中引入复杂锁定的需要。如果保证一段内存在程序的生命周期内不会发生变化,那么多个读取器可以同时访问该内存。他们不可能观察到处于不一致状态的特定数据。
许多函数式编程语言,例如 Lisp、Haskell、Erlang、F# 和 Clojure,就其本质而言鼓励不可变数据结构。正是由于这个原因,当我们转向日益复杂的多线程应用程序开发和多计算机计算机体系结构时,他们重新获得了兴趣。
状态
应用程序的状态可以简单地认为是给定时间点所有内存和 CPU 寄存器的内容。
从逻辑上讲,程序的状态可以分为两种:
- 堆的状态
- 每个执行线程的栈状态
在 C# 和 Java 等托管环境中,一个线程无法访问另一个线程的内存。因此,每个线程都“拥有”其堆栈的状态。堆栈可以被认为是保存局部变量和值类型(struct
)的参数,以及对对象的引用。这些值与外部线程隔离。
但是,堆上的数据可以在所有线程之间共享,因此必须注意控制并发访问。所有引用类型 ( class
) 对象实例都存储在堆上。
在 OOP 中,类实例的状态由其字段决定。这些字段存储在堆上,因此可以从所有线程访问。如果一个类定义了允许在构造函数完成后修改字段的方法,则该类是可变的(不是不可变的)。如果字段不能以任何方式更改,则类型是不可变的。需要注意的是,具有所有 C# readonly
/Javafinal
字段的类不一定是不可变的。这些构造确保引用不能更改,但引用的对象不能更改。例如,一个字段可能对对象列表具有不可更改的引用,但列表的实际内容可能随时修改。
通过将类型定义为真正不可变的,它的状态可以被认为是冻结的,因此该类型对于多个线程的访问是安全的。
在实践中,将所有类型定义为不可变可能很不方便。修改不可变类型的值可能涉及相当多的内存复制。某些语言比其他语言使这个过程更容易,但无论哪种方式,CPU 最终都会做一些额外的工作。许多因素有助于确定复制内存所花费的时间是否超过锁定争用的影响。
很多研究都涉及到不可变数据结构的开发,例如列表和树。当使用这样的结构时,比如说一个列表,“添加”操作将返回一个对添加了新项目的新列表的引用。对上一个列表的引用没有看到任何变化,并且仍然具有一致的数据视图。