并发、交互、可变:三者选二
摘要
文章探讨了编程语言中并发、交互性和可变性之间的固有权衡,通过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 中做的任何事都有可能破坏他们的程序。
相似文章
引用 Mitchell Hashimoto
Mitchell Hashimoto 评论编程语言日益增强的可替代性,以 Bun 从 Zig 重写为 Rust 为例,表明语言已不再是锁定效应的来源。
Typst: Designing for Incrementality
Typst 通过约束记忆化(comemo)和纯函数设计,使语言和编译器协同工作,实现高效的增量编译和实时预览。文章详细介绍了布局缓存、模块评估记忆化、函数纯度以及内省系统的设计思路。
Syntax with Purpose in a Programming Language
这篇文章探讨了编程语言语法设计的重要性,认为语法应准确反映语言的计算模型和心智模型,而非为了熟悉感或简洁性随意拼凑。作者通过分析OCaml、Lisp/Clojure和JavaScript的语法设计,并介绍自己设计的语言Saul,强调了统一性和语义一致性。
七大编程原语言(2022)
一篇文章探讨了七种构成大多数现代编程语言基础的编程语言原型(原语言),认为学习植根于这些原型的基础知识比选择特定语言更重要。
对“长期运行会话”、“生活伴侣”、“LLM-Wiki”、“记忆”的实用批评。解决方案:不可变反思、基于问题和任务的临时会话链、提示模板、独立批评、原型
本文对长期运行的 LLM 会话、生活伴侣型代理以及持久化记忆系统提出了实用批评,指出了隐私、成本、意图丢失和维护等问题,并提出了基于问题的临时会话链和提示模板等替代方案。