并发、交互与可变性,你只能选两个
Concurrency, interactivity, mutability, choose two
在 Common Lisp 程序中,你或许能同时实现并发、交互和可变性,但代价是随时可能因线程冲突导致程序崩溃。想要并发,就得面对死锁和数据竞争;追求交互,就难以避免在运行时访问被并发线程修改的数据;坚持可变性,又会让系统变得极其复杂。C、Rust、Go 等语言选择放弃交互性以换取安全;Python 和 Ruby 通过 GIL 牺牲并发性能来保证交互安全;Erlang 则通过不可变数据和消息传递机制,以复制数据的性能损耗换取并发安全。没有完美的方案,选择编程语言本质上就是在权衡取舍。
正如往常一样,选择一种语言就是选择一组权衡取舍。
- roenxi
但话说回来,天下没有免费的午餐:复制数据很慢,非常慢。当然你可以优化数据表示,避免复制二进制大对象(在 Erlang 中它们是引用计数的),因为那样做根本行不通。但即便如此,速度依然会慢得可怕。
你根本不需要复制数据;数据是不可变的。既然不会改变,为什么要复制?难道我们要为空的 RAM 付费吗?不同的对象可以共享同一个结构。众所周知,Clojure 就是这么做的。理论上,这甚至比可变对象更快,因为你只需要写入正在变化的部分(这和可变对象一样),虽然会损失一些开销(可能微乎其微),但在某些需要对象副本的场景下却能获得巨大的收益,因为那是免费的;除非数据本身真的要被修改,否则根本没有理由真正去复制一份。
- socketcluster
我觉得如果把“交互性”换成“状态性”,这句话会更直观。
并发 + 状态性意味着你无法拥有可变性,否则就会面临并发更新覆盖其他数据的风险。但你可以拥有并发的只读状态,只要不允许并发写入即可。这就是“单一写入原则”,它是“单一事实来源原则”的一种变体。在这种情况下,你不能有并发写入。
如果你有状态性 + 可变性,那就必须放弃并发;这是避免上述并发覆盖问题的另一种方式。
如果你有并发 + 可变性,那只有在没有状态的情况下才可能……你可以拥有多个独立的、分叉的状态副本,但无法拥有一个单一一致的状态。
- mrkeen
这完全讲不通!
放弃交互性 [保留并发、可变性]:
最流行的选择是放弃交互性:既然没有人会随机访问运行时数据,就不存在并发问题。C、Rust、Go,走这条路的语言列表很长。
没人会随机访问运行时数据?“其他线程”不算吗?C 语言里没有并发问题?
放弃可变性
假设你根本无法真正修改数据?与其读取变量的值,不如让运行时系统性地(且安全地)复制该值并返回给你。
为什么要复制任何东西?复制是为了防御突变。这是在充斥着突变的语言中为了安全而采取的措施,而且它是手动开启的,效果也就和 malloc 与 free 这类手动操作差不多。
偷窥一下不可变语言,然后想“那个运行时做了太多复制,这很糟糕!”,这就好比偷窥一下带 GC 的语言,然后想“那个运行时做了太多 malloc 和 free,这很糟糕!”