并发、交互、可变:三者选二
摘要
文章探讨了编程语言中并发、交互性和可变性之间的固有权衡,通过Common Lisp、Python、Ruby和Erlang的例子说明没有语言能完全优化这三者。
暂无内容
查看缓存全文
缓存时间: 2026/07/30 10:48
# 并发、交互性、可变性,三选二
来源:https://www.n16f.net/blog/concurrency-interactivity-mutability-choose-two
你有一个正在运行的 Common Lisp 程序,多个线程监听网络连接,另一些在后台处理数据。你非常小心地确保数据访问没有冲突。并发,没问题。你使用 Emacs 和 [SLIME](https://slime.common-lisp.dev/) 连接到你的服务器,检查其状态。交互性,没问题。你发现一个全局哈希表中包含错误的值;没问题,只需求值一个简单的 `REMHASH` (https://www.lispworks.com/documentation/HyperSpec/Body/f_remhas.htm) 表达式。可变性,没问题。不幸的是,当这个表达式正在求值时,另一个线程恰好读了这个哈希表。游戏结束。
你希望拥有并发,粗略定义为“在同一时间单位内有多个执行线程取得进展”,因为它能让程序更快,处理更多数据。但现在你的程序变得更加复杂,因为你需要保护数据访问以避免死锁、饥饿、内存损坏以及其他不愉快的后果。
你希望拥有交互性,因为随时检查和更新程序状态非常方便。但现在你可以在任何时候访问任何数据,而这些数据可能正被并发线程读取或写入。
你希望拥有可变性,因为它很实用。
但你不能全都要。
## 放弃交互性
最流行的选择是放弃交互性:由于没有人在运行时随意访问数据,因此不存在并发问题。C、Rust、Go,选择这条路的语言数不胜数。
你高效且安全,但失去了在程序执行过程中与之交互的能力。这在开发期间或调试修复复杂系统时会令人沮丧,特别是 [重启并不总是可行](https://flownet.com/gat/jpl-lisp.html) 的场合。
## 放弃并发
当然,你仍然可以有多个线程,但程序中任何数据都不能被同时读取或写入。Python 和 Ruby 是通过使用 GIL(全局解释器锁)来放弃并发的典型例子。
你可以执行多个并发线程,但对 Python 或 Ruby 数据的任何访问都由一个全局锁保护。你可以在 Python shell 或 Ruby 的 [Pry](https://github.com/pry/pry) 中运行程序,安全地随意读写数据。
不幸的是,这很慢。大多数并发程序只需要对全部数据访问中的一小部分进行同步;这部分同步是刻意控制在极小范围内的,因为同步很慢。锁定所有访问会带来非常真实的代价,你必须为此买单。
## 放弃可变性
如果你不能真正改变数据,那会怎么样?运行时不再读取变量的值,而是系统性地(且安全地)复制该值并返回给你。线程之间通过发送值的副本进行通信。当然,你不能直接去修改任意位置的值;但基于消息的方式仍然意味着你可以通过向线程发送消息,在任何时候安全地更新状态。
Erlang 做出了这一选择,理论上这解决了许多问题。你可以运行并发安全的线程(在 Erlang 中称为“进程”),因为它们通过包含复制值的消息进行通信,而不是访问共享内存。而且你可以随意与系统交互。
但同样没有免费的午餐:复制数据很慢。非常慢。当然你可以优化 [数据表示](https://en.wikipedia.org/wiki/Hash_array_mapped_trie),避免复制二进制大对象(在 Erlang 中它们使用引用计数),因为这样做无法承受。但它仍然会 [极其缓慢](https://benchmarksgame-team.pages.debian.net/benchmarksgame/performance/knucleotide.html)。
## 你不能全都要
像往常一样,选择一门语言就是选择一套权衡。大多数对性能敏感的程序会放弃交互性,以便尽可能限制共享数据访问。希望获得交互性便利的开发者必须容忍全局锁或不可变数据模型带来的额外开销。
Common Lisp 开发者当然会吹嘘他们可以全都要,但他们必须始终牢记,在 REPL 中做的任何事都有可能破坏他们的程序。
相似文章
编程语言中的一些好点子
本文讨论了三种编程语言特性:用于在Crystal和TypeScript等静态语言中实现动态感觉的流类型、用于确保Rust内存安全的借用检查,以及用于D语言中不变性检查的契约编程。
引用 Mitchell Hashimoto
Mitchell Hashimoto 评论编程语言日益增强的可替代性,以 Bun 从 Zig 重写为 Rust 为例,表明语言已不再是锁定效应的来源。
快速与硬核代码
本文探讨了大语言模型如何降低编程语言选择的摩擦,促使开发者采用如 Rust 和 Zig 等面向性能的语言来开发快速、小型的软件,并使得处理以前被认为困难的技术成为可能。
一个固执的程序员尝试语言服务器
一位开发者反思传统的编码工作流程,并探讨在Haskell中使用语言服务器如何能近似Lisp程序员的交互体验。
编程语言如何影响 token 效率和正确性?
Dan Luu 批评了关于动态语言对 LLM 更节省 token 的说法,指出现有评估中的缺陷,并强调需要更好的基准测试方法。