返回文章列表

文章

STM:为什么 Rust 和 Go 没有把它作为核心并发机制?

从 STM 的事务模型出发,对比 Haskell、Rust 与 Go 的并发设计哲学,重点解释 Rust 所有权、Borrowing、Send/Sync 与共享状态边界,并分析 STM 的优势、限制与系统设计价值。

目录
  1. 一、STM 到底解决什么问题?
  2. 使用 Mutex
  3. 二、STM 的核心不是“无锁”,而是可组合性
  4. Composability —— 可组合性
  5. 三、Haskell 为什么特别适合 STM?
  6. 因此 Haskell STM 不只是简单提供一个 transaction {} 语法,而是建立了一整套完整的并发状态抽象。
  7. 四、Rust 有没有 STM?
  8. 五、Go 有没有 STM?
  9. 六、为什么 Rust 没有把 STM 放进语言核心?
  10. 七、Rust 所有权到底解决了什么?
  11. 用所有权减少共享。
  12. 八、Borrowing 进一步定义了“访问边界”
  13. 九、但 Ownership 并没有消灭共享状态
  14. 这一层。
  15. 十、Arc 只解决“共享所有权”,不解决“共享可变访问”
  16. 十一、Send 和 Sync 是 Rust 共享状态边界的重要组成部分
  17. 很多情况下,这些问题在编译期就能够得到答案。
  18. 十二、Rust 的“共享状态边界”在哪里?
  19. 第一层:Ownership Boundary
  20. 数据直接转移。
  21. 第二层:Borrowing Boundary
  22. 第三层:Thread-Safety Boundary
  23. 这就是 Rust 与 STM 非常重要的分界。
  24. 十三、因此 Rust 的并发模型其实是逐层升级的
  25. 这时候才真正开始触碰 STM 所解决的问题。
  26. 十四、Rust Channel 为什么也很重要?
  27. 十五、STM 与 Rust 的真正关系
  28. 对复杂共享状态提供更强的可组合性。
  29. 整体事务化。 这正是 Rust 当前标准同步原语没有直接提供的抽象。
  30. 十六、STM 的另一个关键问题:事务代码可能重复执行
  31. 最终可能出现重复消息。
  32. 十七、这也是 STM 与副作用之间最重要的边界
  33. 思想可以联系起来理解。
  34. 十八、为什么 Rust 更倾向 Mutex / Atomic / Lock-free?
  35. 仍然非常有价值。
  36. 十九、为什么 Go 更没有动力采用 STM?
  37. 二十、三种语言其实是三条不同的并发路线
  38. 二十一、STM 最适合什么场景?
  39. 二十二、STM 不适合什么?
  40. 二十三、STM 与数据库事务为什么很像?
  41. 它们不完全等价,但共享很多重要的并发控制思想。
  42. 二十四、STM 与 Lock-free 不是一回事
  43. 二十五、为什么 STM 依然值得学习?
  44. 二十六、从系统设计角度,应该如何理解 STM?
  45. 二十七、一张图看懂三种模型
  46. 二十八、最终结论
  47. 二十九、为什么 STM 依然值得学习?
  48. 延伸阅读
  49. 一句话总结

STM(Software Transactional Memory,软件事务内存)是一种不同于 Mutex、Channel 和 Atomic 的并发编程模型。 它的核心思想很简单:

把一组对共享内存的读写操作组织成一个事务,并由运行时负责冲突检测、提交和重试。 因此,STM 真正解决的问题并不是“如何替代锁”,而是: 如何以一种可组合的方式,对复杂共享状态进行原子更新。 这也是理解 Haskell、Rust 和 Go 三种并发模型差异的一个很好切入点。


一、STM 到底解决什么问题?#

假设内存中存在两个账户:

Alice = 100
Bob   = 50

现在需要执行:

Alice -= 30
Bob   += 30

使用 Mutex#

传统做法需要显式管理锁:

lock Alice
lock Bob

Alice -= 30
Bob   += 30

unlock Bob
unlock Alice

一旦系统中存在多个共享对象,就必须开始考虑:

锁的获取顺序
锁的持有范围
锁竞争
死锁
中间状态

而 STM 的思路不同:

Transaction
    ↓
读取共享状态
    ↓
执行计算
    ↓
记录读集 / 写集
    ↓
提交前验证
    ↓
是否冲突?
 ┌────┴────┐
否         是
│           │
Commit     Retry

事务只有在验证成功后才真正修改共享状态。 因此,程序员表达的是:

“这几个状态需要一起修改”

而不是:

“我要先锁 A,再锁 B,最后按什么顺序释放”

二、STM 的核心不是“无锁”,而是可组合性#

这是理解 STM 最重要的一点。 很多人第一次接触 STM 时,会直接把它理解成:

STM = 不使用 Mutex 的并发编程。 实际上,这并不是 STM 最有价值的地方。 STM 真正重要的能力是:

Composability —— 可组合性#

假设系统中有:

withdraw()
deposit()
transfer()

使用锁时,每个函数可能都有自己的锁策略。 当这些操作组合起来时,需要重新考虑:

谁持有什么锁?
锁的获取顺序是什么?
是否可能死锁?
中间状态是否会被其他线程观察到?

STM 则可以把多个事务操作继续组合成一个更大的事务。 抽象地看:

withdraw()
    +
deposit()
    ↓
larger transaction

最终仍然具有事务的原子性。 所以 STM 更准确的定位是:

一种用于组合复杂共享状态操作的并发控制模型。


三、Haskell 为什么特别适合 STM?#

Haskell 是 STM 最经典的语言之一。 GHC 提供了完整的 STM 抽象,包括:

TVar
atomically
retry
orElse

典型代码:

atomically $ do
    a <- readTVar alice
    b <- readTVar bob

    writeTVar alice (a - 30)
    writeTVar bob   (b + 30)

这里最值得关注的是 retry。 例如余额不足:

withdraw amount = do
    balance <- readTVar account

    if balance >= amount
        then writeTVar account (balance - amount)
        else retry

retry 并不是:

while balance < amount:
    sleep()

它表达的是:

当前事务依赖的条件还没有满足,因此暂停当前事务;当相关事务状态发生变化后,再重新尝试。 这就使得“等待条件成立”本身也成为事务模型的一部分。 而 orElse 则可以表达:

先尝试事务 A
如果 A 无法继续
    ↓
尝试事务 B

因此 Haskell STM 不只是简单提供一个 transaction {} 语法,而是建立了一整套完整的并发状态抽象。#

四、Rust 有没有 STM?#

有。 Rust 生态存在第三方 STM 实现,例如 stm crate,提供了与 Haskell STM 类似的事务抽象。 概念上可以表达:

atomically(|tx| {
    let a = alice.read(tx)?;
    let b = bob.read(tx)?;

    alice.write(tx, a - 30)?;
    bob.write(tx, b + 30)?;

    Ok(())
});

具体实现通常需要维护事务元数据,例如:

Read Set
Write Set
Version
Validation Information

事务执行过程中并不一定立即修改真实共享状态,而是先记录操作。 提交时:

验证读集
    ↓
检测冲突
    ↓
成功 → Commit
失败 → Retry

所以:

Rust 并不是做不到 STM,而是没有选择把 STM 作为主流并发模型。


五、Go 有没有 STM?#

同样有。 Go 社区也存在第三方 STM 实现,可以提供类似:

Atomic Transaction
Transactional Variable
Retry

这样的机制。 但 STM 同样不是 Go runtime 或标准库中的核心并发抽象。 这是因为 Go 选择的主路径并不是“让共享内存变得事务化”,而是:

尽量通过消息传递降低共享状态的复杂度。


六、为什么 Rust 没有把 STM 放进语言核心?#

这并不是因为 Rust 无法实现 STM。 更重要的原因是:

Rust 已经选择了另一套解决共享状态问题的核心机制。


七、Rust 所有权到底解决了什么?#

理解 Rust 与 STM 的关系,首先必须把一个非常容易混淆的问题拆开:

“数据属于谁?”与“数据是否被多个线程共享?”其实是两个不同的问题。 Rust 的 Ownership 首先解决的是资源归属权。 例如:

let task = Task::new();
worker(task);

这里 task 被 move 到 worker。 可以把它理解成:

Thread A
   │
   │ ownership transfer
   ▼
Thread B

转移完成以后,原来的线程不再拥有这份数据。 于是很多问题根本不需要:

lock
atomic
transaction

因为系统直接规定:

同一时刻只有一个地方拥有这份可变状态。 这就是 Rust 最重要的并发设计之一:

用所有权减少共享。#


八、Borrowing 进一步定义了“访问边界”#

Ownership 解决的是“谁拥有”。 Borrowing 解决的是:

在不转移所有权的情况下,谁可以访问。 Rust 的两个核心形式:

&T
&mut T

可以粗略理解为:

&T
    → 只读共享

&mut T
    → 独占可变访问

例如:

fn read_config(config: &Config) {
    // 多个调用者可以同时读取
}

而:

fn update_config(config: &mut Config) {
    // 当前调用者独占可变访问
}

这背后的关键规则可以概括成:

多个 &T
    或
一个 &mut T

而不能同时存在:

&T + &mut T

这种限制非常重要,因为它把大量“共享内存并发错误”提前变成了编译错误。 所以 Rust 的思路不是:

共享以后再想办法保证安全

而是:

先限制谁能访问
        ↓
再考虑如何共享

九、但 Ownership 并没有消灭共享状态#

这是特别容易被误解的一点。 Rust 并不是:

“有 Ownership,所以不存在共享内存。” 实际上完全不是。 Rust 可以非常自然地构造:

Arc<T>
Arc<Mutex<T>>
Arc<RwLock<T>>
Arc<AtomicU64>

例如:

use std::sync::{Arc, Mutex};

let state = Arc::new(Mutex::new(0));

这意味着:

        Arc
     ┌───┴───┐
     │       │
Thread A   Thread B
     │       │
     └───┬───┘
         ▼
      Mutex<T>
         │
      shared state

所以:

Ownership 并不是消灭共享状态,而是首先尝试把“唯一拥有”作为默认模型。 只有当需求本身确实要求多个执行单元共享数据时,才重新进入:

Arc
    ↓
Mutex / RwLock / Atomic

这一层。#

十、Arc 只解决“共享所有权”,不解决“共享可变访问”#

这一点尤其值得区分。 很多人看到:

Arc<T>

就认为“线程安全了”。 实际上 Arc 只解决:

多个线程如何共同持有同一份数据。 它并不自动解决: 多个线程如何安全地修改这份数据。 所以可以把 Rust 的共享状态层次看成:

Ownership
    ↓
Shared Ownership
    ↓
Arc
    ↓
Shared Mutable State
    ↓
Mutex / RwLock / Atomic

而 STM 则位于更高一层:

多个 Shared Mutable State
          ↓
复杂状态关系
          ↓
Transactional Composition
          ↓
STM

这就是为什么 STM 与 Rust 的关系不是:

STM vs Ownership

而更接近:

Ownership
   ↓
是否需要共享?
   ↓
Shared State
   ↓
Mutex / Atomic / RwLock
   ↓
是否需要复杂事务组合?
   ↓
STM

十一、Send 和 Sync 是 Rust 共享状态边界的重要组成部分#

仅仅有 Ownership 和 Borrowing 还不够。 Rust 还需要回答:

这份数据能不能跨线程发送? 以及: 这份数据能不能被多个线程通过共享引用访问? 这就是:

Send
Sync

可以用一个非常粗略但实用的方式理解:

Send
→ T 的所有权可以安全地发送到另一个线程

Sync
→ &T 可以安全地在线程之间共享

于是 Rust 的并发安全模型实际上变成:

Ownership
       ↓
Borrowing
       ↓
Send / Sync
       ↓
允许跨线程共享的边界
       ↓
Arc / Mutex / Atomic / Channel

这意味着 Rust 的共享状态并不是一个模糊的“大家随便访问”。 而是存在非常清晰的类型边界:

这个类型能不能跨线程?
这个引用能不能在线程间共享?
这个状态是否拥有内部可变性?
这个修改是否需要同步?

很多情况下,这些问题在编译期就能够得到答案。#

十二、Rust 的“共享状态边界”在哪里?#

可以把 Rust 并发设计理解成三道边界。

第一层:Ownership Boundary#

谁拥有数据?

默认倾向:

One Owner

例如:

let data = Vec::new();
worker(data);

数据直接转移。#

第二层:Borrowing Boundary#

谁可以暂时访问数据?

主要是:

&T
&mut T

这一步主要控制:

共享读
独占写
生命周期

第三层:Thread-Safety Boundary#

这个数据能不能跨线程?

主要体现为:

Send
Sync

一旦真的需要共享可变状态:

Arc<Mutex<T>>
Arc<RwLock<T>>
Arc<Atomic<T>>

就说明程序已经主动越过了:

“单一所有者 + 借用” 这条简单边界。 进入共享状态以后,Rust 并没有继续假装这个问题不存在,而是要求你显式选择:

Mutex?
RwLock?
Atomic?
Channel?
Actor?

这就是 Rust 与 STM 非常重要的分界。#

十三、因此 Rust 的并发模型其实是逐层升级的#

可以把整个过程画成:

     是否需要共享?
           │
    ┌──────┴──────┐
    │             │
   否             是
    │             │
    ▼             ▼
Ownership        Arc
    │             │
    │       ┌─────┴─────┐
    │       │           │
    │      Read        Write
    │       │           │
    │      RwLock     Mutex
    │                     │
    └─────────┬───────────┘
              │
       状态关系复杂?
              │
        ┌─────┴─────┐
        │           │
       否           是
        │           │
     Lock/Atomic    STM

这表达了一个重要的工程原则:

不要一上来就把所有东西都设计成 Arc<Mutex<T>>。 Arc<Mutex<T>> 很方便,但它意味着你已经进入了:

Shared Ownership
+
Shared Mutable State
+
Runtime Synchronization

这个世界。 而 Rust 更希望你优先考虑:

Can I move it?
Can I borrow it?
Can I send it?
Can I avoid sharing it?

只有这些答案都是否定的,才需要:

Arc
Mutex
RwLock
Atomic

而当共享状态进一步演化成:

多个对象
多个条件
需要组合
需要原子提交

这时候才真正开始触碰 STM 所解决的问题。#

十四、Rust Channel 为什么也很重要?#

Rust 还有另一条非常重要的路线:

Ownership Transfer
+
Message Passing

例如:

tx.send(data)?;

这里不是让两个线程共同修改:

Arc<Mutex<Data>>

而是:

Thread A
   │
   │ ownership
   ▼
Channel
   │
   ▼
Thread B

数据的所有权发生转移。 这和 Go 的 Channel 思想有相似之处,但 Rust 额外把 Ownership 纳入类型系统。 因此可以把 Rust 并发设计看成两个方向:

方向 A:减少共享

Ownership
Channel
Message Passing

方向 B:管理共享

Arc
Mutex
RwLock
Atomic

而 STM 又是:

方向 C:事务化共享状态

Transactional Memory

十五、STM 与 Rust 的真正关系#

因此,不应该问:

“为什么 Rust 没有 STM?” 更准确的问题是: Rust 已经有 Ownership、Borrowing、Send/Sync、Mutex、Atomic、Channel 等一整套共享状态控制机制,那么 STM 还能增加什么? 答案主要是:

对复杂共享状态提供更强的可组合性。#

例如存在:

Account
Inventory
Quota
Order

并且要求:

余额足够
AND
库存存在
AND
额度足够

同时修改:

balance -= 50
inventory -= 1
quota -= 50

这时:

Arc<Mutex<Account>>
Arc<Mutex<Inventory>>
Arc<Mutex<Quota>>

开始出现复杂的锁组合问题。 STM 提供的是:

read
check
write
commit

整体事务化。 这正是 Rust 当前标准同步原语没有直接提供的抽象。#

十六、STM 的另一个关键问题:事务代码可能重复执行#

STM 有一个非常重要的特征:

Execute
   ↓
Conflict
   ↓
Abort
   ↓
Retry

因此,一个事务内部的代码可能执行多次。 例如:

atomically(|tx| {
    println!("hello");
    // ...
});

如果发生冲突,理论上可能得到:

hello
hello
hello

这说明一个重要问题:

事务内部的代码必须允许重复执行。 因此下面这些操作就非常危险:

HTTP request
Kafka publish
MQTT publish
文件写入
日志输出
调用外部服务

因为这些操作通常不能随着事务回滚。 例如:

Transaction
    ↓
Kafka.publish()
    ↓
Conflict
    ↓
Retry
    ↓
Kafka.publish() again

最终可能出现重复消息。#

十七、这也是 STM 与副作用之间最重要的边界#

可以把代码分成两类:

Transactional State
    │
    ├── 可回滚
    ├── 可重复执行
    └── 只修改事务状态

External Effects
    │
    ├── 网络
    ├── 文件
    ├── Kafka / MQTT
    ├── 数据库外部调用
    └── 其他 IO

更合理的系统结构是:

        STM Transaction
             │
    ┌────────┴────────┐
    │                 │
State Update       Decision
    │                 │
    └────────┬────────┘
             │
           Commit
             │
             ▼
      External Effects

也就是:

先提交内部状态,再执行不可回滚的外部副作用。 这和很多现代系统中的:

Transactional Outbox
State Machine
Event Publishing

思想可以联系起来理解。#

十八、为什么 Rust 更倾向 Mutex / Atomic / Lock-free?#

另一个原因是性能模型。 STM 通常是一种乐观并发控制机制:

执行
 ↓
记录读写集
 ↓
验证
 ↓
Commit

如果并发冲突很低:

STM
 ↓
很自然

但是如果冲突频繁:

执行 100 μs
    ↓
冲突
    ↓
重新执行 100 μs
    ↓
再次冲突
    ↓
再次执行

一个事务可能计算多次,但最终只成功提交一次。 这可能带来:

更多 CPU
更多事务管理开销
更多 cache traffic
更复杂的延迟行为

而 Rust 经常用于:

系统软件
网络服务
数据库
消息系统
Runtime
基础设施
高性能计算

这些场景通常非常关注:

Latency
Throughput
Memory Usage
Cache Behavior
Contention
Allocation

因此显式的:

Mutex
RwLock
Atomic
Sharded Lock
Lock-free Structure

仍然非常有价值。#

十九、为什么 Go 更没有动力采用 STM?#

Go 的方向更加明显:

Goroutine
    ↓
Channel
    ↓
Message Passing

Go 经典的并发思想可以概括为:

不要通过共享内存通信,而是通过通信共享内存。 这并不意味着 Go 不支持共享内存。 Go 仍然提供:

sync.Mutex
sync.RWMutex
sync.Once
sync/atomic

只是 Go 更倾向于让系统结构变成:

Goroutine A
      │
      │ Message
      ▼
    Channel
      │
      ▼
Goroutine B

而不是:

Goroutine A ─┐
              ├── Shared State
Goroutine B ─┘

所以 Go 与 STM 在设计哲学上存在明显差异:

STM
    ↓
让共享状态更容易管理

Go
    ↓
尽量减少直接共享状态

二十、三种语言其实是三条不同的并发路线#

可以用下面的方式理解:

Haskell
    ↓
Shared State
    ↓
STM
    ↓
Transactional + Composable Concurrency
Go
    ↓
Reduce Shared State
    ↓
Goroutine + Channel + CSP
    ↓
Message Passing
Rust
    ↓
Ownership
    ↓
Reduce Shared Mutable State
    ↓
Send / Sync
    ↓
Mutex / Atomic / Channel

因此并不是:

Haskell STM
    ↓
Rust / Go 缺失

而是:

Haskell → 选择 STM 作为核心并发抽象

Go     → 选择 CSP / Channel

Rust   → 选择 Ownership + Type System

二十一、STM 最适合什么场景?#

STM 特别适合这样的状态管理问题:

多个共享变量
        +
复杂一致性条件
        +
需要原子更新

例如:

库存
余额
额度
资源池
任务队列
状态机
多个关联计数器

比如一次操作要求:

库存 > 0
AND
余额 >= 50
AND
额度 >= 50

然后:

库存 -= 1
余额 -= 50
额度 -= 50

这实际上非常适合 STM:

Read
 ↓
Check
 ↓
Update
 ↓
Atomic Commit

因为这里真正的问题不是“某个变量怎么加锁”,而是:

多个状态之间的关系必须作为一个整体保持一致。


二十二、STM 不适合什么?#

STM 不适合把整个业务逻辑都包进去。 尤其不适合直接执行:

HTTP Request
RPC Call
Kafka Publish
MQTT Publish
文件写入
外部数据库调用
第三方 API

因为 STM 的事务可能:

Commit
或者
Rollback + Retry

而网络请求无法简单 rollback。 因此,如果系统存在大量外部副作用,通常还需要:

Transaction
    ↓
Commit State
    ↓
Event / Outbox
    ↓
External Side Effect

这时问题就从 STM 延伸到了:

Transaction
    ↓
Event
    ↓
Message Broker
    ↓
Distributed System

二十三、STM 与数据库事务为什么很像?#

这是 STM 最值得深入研究的地方之一。 STM 和数据库事务并不是同一个东西,但两者存在非常强的思想对应。 可以看到一条很有意思的技术链:

STM
 ↓
Read Set / Write Set
 ↓
Conflict Detection
 ↓
Optimistic Concurrency Control
 ↓
MVCC
 ↓
Database Transaction

典型流程都类似:

读取状态
    ↓
执行计算
    ↓
产生临时修改
    ↓
检测冲突
    ↓
成功提交 / 放弃

进一步向底层研究,还可以继续连接:

STM
 ↓
OCC
 ↓
MVCC
 ↓
Lock-free
 ↓
Hardware Transactional Memory

它们不完全等价,但共享很多重要的并发控制思想。#

二十四、STM 与 Lock-free 不是一回事#

这两个概念也经常被混淆。 STM 关注的是:

如何组织事务以及处理事务冲突。 Lock-free 关注的是: 如何通过原子操作设计数据结构,使系统整体不会因为某个线程阻塞而停止进展。 所以:

STM
    ≠
Lock-free

STM 甚至完全可以使用锁或原子操作作为内部实现机制。 真正需要比较的是:

编程模型
性能模型
冲突模型
可组合性
实现复杂度
进度保证

而不是简单比较:

STM vs Mutex

二十五、为什么 STM 依然值得学习?#

如果只是为了日常写 Rust 或 Go:

Mutex
Atomic
Channel
Send / Sync
Goroutine

显然更加常用。 但是从系统设计和并发控制理论的角度,STM 非常值得学习。 它可以帮助建立以下几个核心概念:

Atomicity
Isolation
Conflict Detection
Optimistic Concurrency
Retry
Commit
Composability

理解这些概念以后,再去学习:

数据库事务
MVCC
消息系统
Actor
Scheduler
Runtime
Lock-free Algorithm
分布式事务

会容易很多。 从系统设计角度看,STM 最值得学习的不是某一个 Rust 或 Go 库,而是它背后的思想:

把复杂并发状态操作提升到“可组合事务”的抽象层。


二十六、从系统设计角度,应该如何理解 STM?#

不要把 STM 理解成:

“一个比 Mutex 更先进的锁。” 更准确的理解是: STM 是另一套并发编程模型。 它回答的是: 当系统中存在多个共享状态,并且这些状态之间存在复杂的一致性关系时,能不能把它们的更新过程抽象成一个可组合的事务? Haskell 的答案是:

TVar
+
atomically
+
retry
+
orElse

Go 更倾向于:

Goroutine
+
Channel
+
Message Passing

Rust 更倾向于:

Ownership
+
Borrowing
+
Send / Sync
+
Mutex / Atomic / Channel

因此,这三种语言不是简单地“缺少”或“拥有”某个并发 API。 它们实际上是在回答同一个问题:

如何管理并发系统中的状态? 只是选择了不同的抽象层次。


二十七、一张图看懂三种模型#

                   并发共享状态
                        │
         ┌──────────────┼──────────────┐
         │              │              │
         ▼              ▼              ▼
      Haskell          Go            Rust
         │              │              │
        STM            CSP         Ownership
         │              │              │
         ▼              ▼              ▼
   Transaction      Message       Type System
         │          Passing            │
         ▼              ▼              ▼
     TVar          Channel       Send / Sync
         │              │              │
         ▼              ▼              ▼
retry / orElse      Select       Mutex / Atomic

二十八、最终结论#

Rust 和 Go 不是没有 STM。 两者都可以通过第三方库实现 STM。 真正的区别在于:

STM 没有成为 Rust 和 Go 的核心并发抽象。 Rust 的关键设计路径可以概括为:

Ownership
    ↓
Borrowing
    ↓
Send / Sync
    ↓
是否必须共享?
    ↓
Arc / Mutex / RwLock / Atomic / Channel
    ↓
是否存在复杂的跨状态原子更新?
    ↓
STM

所以 Rust 的独特之处并不是“没有共享状态”,而是:

它把共享状态变成一个需要显式跨越的边界。 从设计角度看,Rust 会不断要求你回答:

谁拥有数据?
谁可以读取?
谁可以修改?
是否真的需要共享?
能否通过 ownership transfer?
能否通过 channel 传递?
如果必须共享,应该用 Mutex、RwLock 还是 Atomic?
如果多个共享状态必须组成一个原子操作,是否需要事务模型?

这与 Haskell STM 的起点不同。 Haskell 更自然地表达:

共享状态
    ↓
TVar
    ↓
STM
    ↓
atomically

Rust 更自然地表达:

数据
 ↓
Ownership
 ↓
Borrowing
 ↓
Send / Sync
 ↓
Shared State
 ↓
Synchronization Primitive

Go 则更自然地表达:

Goroutine
    ↓
Channel
    ↓
Message Passing

因此:

Haskell 更关注“如何把共享状态事务化并进行组合”;Go 更关注“如何通过消息传递减少直接共享”;Rust 更关注“如何通过所有权和类型系统明确共享状态的边界”。


二十九、为什么 STM 依然值得学习?#

如果只是为了日常写 Rust 或 Go:

Mutex
Atomic
Channel
Send / Sync
Goroutine

显然更加常用。 但是从系统设计和并发控制理论的角度,STM 非常值得学习。 它可以帮助建立以下几个核心概念:

Atomicity
Isolation
Conflict Detection
Optimistic Concurrency
Retry
Commit
Composability

理解这些概念以后,再去学习:

数据库事务
MVCC
消息系统
Actor
Scheduler
Runtime
Lock-free Algorithm
分布式事务

会容易很多。 从系统设计角度看,STM 最值得学习的不是某一个 Rust 或 Go 库,而是它背后的思想:

把复杂并发状态操作提升到“可组合事务”的抽象层。


延伸阅读#

一句话总结#

Haskell 通过 STM 把共享状态组织成可组合事务;Go 更倾向于通过 CSP 和 Channel 减少直接共享;Rust 则通过 Ownership、Borrowing、Send/Sync 与显式同步原语管理共享状态。STM 不是 Rust 或 Go 缺少的能力,而是它们没有选择把 STM 作为核心并发模型。