并发、交互性、可变性三选二2026 年 7 月 26 日在运行一个 Common Lisp 程序时多个线程监听网络连接其他线程在后台处理数据小心翼翼确保无数据访问冲突并发满足。使用 Emacs 和 SLIME 连接到服务器检查状态交互性满足。发现全局哈希表包含错误值只需计算一个简单的 REMHASH 表达式可变性满足。然而在计算该表达式时另一个线程恰好正在读取该哈希表游戏结束。需要并发即“在同一时间单位内有多个执行线程在推进”因为它能让程序运行得更快处理更多数据。但程序变得复杂需保护数据访问避免死锁、饥饿、内存损坏等不良后果。需要交互性因为能随时检查和更新程序状态很方便。但可随时访问的数据可能正被并发线程读写。需要可变性因为它很实用。但不能三者兼得。放弃交互性最常见的选择是放弃交互性由于没人会随机访问运行时数据也就不存在并发问题。采用这种方式的编程语言有很多比如 C、Rust、Go。这样做既高效又安全但失去了在程序执行期间与其交互的能力。这在开发过程中会令人沮丧在调试和修复复杂系统时也很麻烦因为并非总能通过重启来解决问题。放弃并发当然仍然可以使用多个线程但程序中没有任何数据可以同时被读写。Python 和 Ruby 是通过使用全局解释器锁GIL来放弃并发的典型例子。可以执行多个并发线程但对 Python 或 Ruby 数据的任何访问都受到全局锁的保护。可以在 Python 交互式 shell 中运行程序或者在 Ruby 中使用 Pry安全地随意读写数据。不幸的是这样做速度很慢。大多数并发程序只需要对一小部分数据访问进行同步因为同步操作本身就很慢。对所有访问都加锁会带来实实在在的性能开销。放弃可变性如果不能真正改变数据会怎样呢不是直接读取变量的值而是让运行时系统安全地复制该值并返回。线程之间通过传递值的副本进行通信。当然不能直接修改任何地方的值但基于消息的方式意味着仍然可以通过向线程发送消息来安全地更新状态。Erlang 就做出了这样的选择从理论上讲它解决了很多问题。可以安全地运行并发线程在 Erlang 中称为“进程”因为它们通过包含复制值的消息进行通信而不是访问共享内存。而且可以随意与系统进行交互。但天下没有免费的午餐复制数据很慢非常慢。当然可以优化数据表示避免复制二进制数据块在 Erlang 中它们是引用计数的因为复制这些数据是不可接受的。但程序仍然会慢得可怕。你不能三者兼得和往常一样选择编程语言就是在做一系列的权衡。大多数对性能敏感的程序会放弃交互性以便尽可能减少对共享数据的访问。想要交互性带来便利的开发者则必须忍受全局锁或不可变数据模型带来的额外开销。Common Lisp 开发者当然会吹嘘他们可以三者兼得但他们必须时刻牢记在 REPL 中进行的任何操作都有可能破坏他们的程序。那么该如何选择编程语言呢