Chapter 15. An introduction to message-passing concurrency
覆盖shared state风险、mailbox/agent protocol、functional API与LOB topology,把serialization、capacity、failure和delivery guarantees纳入设计。
学习目标
- 能比较lock、atomic、immutable snapshot与single-owner agent,判断各自保护的invariant和failure mode
- 能设计typed mailbox protocol、bounded capacity、reply/cancellation与supervision,复现ordering和crash behavior
- 能分析LOB系统的partition key、consistency boundary和load topology,实现functional API与agent runtime分离
机制总览
Chapter 15. An introduction to message-passing concurrency:机制路径
- 1
为什么不共享Memory仍然需要Concurrency设计
Message passing把“多个threads直接改同一对象”改成“一个owner按顺序处理immutable messages”。它缩小race surface,却没有消灭并发问题:senders仍并行,mailbox可能无限增长,消息可能duplicate/out-of-order,han…
- 2
The need for shared mutable s…
许多invariants横跨多个值,例如余额与reserved amount、inventory与orders、connection与sequence。Lock可把临界区原子化,但所有访问路径必须遵守同一lock order;atomic适合单word update,复杂invariant仍需pro…
- 3
Understanding message-passing…
Mailbox接收messages,owner逐个dispatch并更新private state。Local mailbox通常提供enqueue后按某种顺序处理,但来自不同senders的全局顺序未必定义;remote transport还加入serialization、network part…
章级决策实验
Chapter 15. An introduction to message-passing concurrency:机制与证据
切换《Chapter 15. An introduction to message-passing concurrency》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 为什么不共享Memory仍然需要Concurrency设计
Message passing把“多个threads直接改同一对象”改成“一个owner按顺序处理immutable messages”。它缩小race surface,却没有消灭并发问题:senders仍并行,mailbox可能无限增长,消息可能duplicate/out-of-order,han…
可核验证据
以确定输入重复运行「为什么不共享Memory仍然需要Concurrency设计」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
学完《Chapter 15. An introduction to message-passing concurrency》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 15. An introduction to message-passing concurrency:失效与核验
为什么不共享Memory仍然需要Concurrency设计
典型失效
若把「为什么不共享Memory仍然需要Concurrency设计」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「为什么不共享Memory仍然需要Concurrency设计」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
The need for shared mutable s…
典型失效
若把「The need for shared mutable s…」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「The need for shared mutable s…」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
Understanding message-passing…
典型失效
若把「Understanding message-passing…」只写成函数式术语而不隔离副作用、状态和失败分支,组合后的程序仍会依赖隐藏时序,无法从输入稳定推导结果。
核验证据
以确定输入重复运行「Understanding message-passing…」的最小管线,用属性测试、状态快照和副作用调用轨迹核对返回值、失败传播与资源边界。
为什么不共享Memory仍然需要Concurrency设计
Message passing把“多个threads直接改同一对象”改成“一个owner按顺序处理immutable messages”。它缩小race surface,却没有消灭并发问题:senders仍并行,mailbox可能无限增长,消息可能duplicate/out-of-order,handler可能crash,reply可能timeout,remote delivery更不等于exactly once。正确性从lock discipline转为protocol和ownership discipline。
先预测:actor一次处理一个message就没有lost update吗;reply timeout表示command未执行吗;immutable message可以安全包含mutable List吗;unbounded mailbox能吸收流量峰值吗;remote actor天然exactly-once吗。答案都是否。
↡由一个执行上下文独占mutable state,其他参与者只能通过messages请求transition的并发边界。The need for shared mutable state
许多invariants横跨多个值,例如余额与reserved amount、inventory与orders、connection与sequence。Lock可把临界区原子化,但所有访问路径必须遵守同一lock order;atomic适合单word update,复杂invariant仍需protocol;immutable snapshot允许lock-free reads,但writers仍要CAS/version;agent把writes串行化并让state不逃出owner。
Either<Error, AccountState> Decide(
AccountState state,
AccountMessage message) => message switch
{
Debit debit when state.Balance >= debit.Amount =>
Right(state with { Balance = state.Balance - debit.Amount }),
Debit => Left(Error.InsufficientFunds),
_ => Left(Error.UnsupportedMessage)
};先定义invariant与contention profile,再选机制。单个counter用Interlocked;read-heavy configuration用immutable atomic replace;短小local aggregate可lock;有明确identity、commands和serial transition的entity适合agent。不要为了“无锁”把简单问题拆成异步protocol,也不要因agent流行就隐藏需要跨entity atomicity的事实。
↡Lock、CAS或mailbox真正保证不可被interleaving破坏的业务关系及其作用范围。切换lock、atomic、immutable与agent
Understanding message-passing concurrency
Mailbox接收messages,owner逐个dispatch并更新private state。Local mailbox通常提供enqueue后按某种顺序处理,但来自不同senders的全局顺序未必定义;remote transport还加入serialization、network partition与redelivery。Protocol必须写message identity、sender/causation、expected version、deadline和reply shape。
Tell是fire-and-forget command;Ask建立reply Task,但timeout只表示caller没收到及时reply,handler可能稍后成功。若caller重试,command id/idempotency需要防重。Cancellation同样不能撤回已入队command,除非protocol定义cancel message和owner处理规则;不要把token cancellation误当作distributed rollback。
↡定义message schema、identity、ordering、reply、timeout、dedup和version expectations的并发通信契约。切换send、handle、reply与fail
Functional APIs, agent-based implementations
Caller不应依赖具体actor runtime。暴露Func<Command, Task<Either<Error,Reply>>>或domain interface,implementation可用Channel、TPL Dataflow、actor framework或单线程loop。Pure handler保持(State, Message) -> (State, Effects/Reply),runtime负责queue、serialization、cancellation、logging和effect interpretation。
public interface IAccount
{
Task<Either<AccountError, Receipt>> Handle(
AccountCommand command,
CancellationToken token);
}
public sealed record Transition(
AccountState State,
IReadOnlyList<AccountEffect> Effects,
Either<AccountError, Receipt> Reply);这种分离让unit test直接验证handler,不启动threads;contract test再验证runtime的ordering、reply和shutdown。Effects先表示为data,例如PersistEvent、PublishNotification,再由interpreter执行。若effect成功后process在state checkpoint前crash,恢复语义必须明确:event-sourced owner可从durable log重建,in-memory owner可能丢state,outbox处理publish consistency。
↡Domain caller只看到typed command/reply函数,而mailbox、scheduler和runtime作为可替换implementation detail。Mailbox capacity and backpressure
Unbounded mailbox把overload变成latency和memory growth;bounded mailbox必须定义full policy:wait、reject、drop oldest/latest或route dead letter。Command通常不能静默drop,telemetry sample可能允许latest wins。Capacity由service time、burst、SLO和memory per message推导,并用queue age而非只看length告警。
Fairness也重要:单个slow command会head-of-line block;按entity sharding可并行不同owners,但hot key仍串行。Long I/O handler若await时允许reentrancy,state可能被其他message改变;若不允许reentrancy,mailbox吞吐受I/O限制。可把I/O拆为effect + completion message,带state version/correlation,回来时重新验证context。
Failure, supervision, and durable ownership
Handler抛异常时选择resume、restart、stop或escalate;每种都要说明当前message、private state和mailbox如何处理。Blind restart可能反复处理poison message;blind resume可能保留corrupt state。通常unexpected fault记录cause,将message隔离/dead-letter,按durable snapshot/event log恢复known-good state,再由supervisor限速重启。
Local agent进程退出就丢mailbox/state,不能承担需要durability的事实。持久命令先写broker/log,再由consumer owner处理并checkpoint;delivery至少一次时handler必须idempotent。Exactly-once business effect通常靠command id、unique constraint、transactional outbox和dedup,不靠runtime宣传语。
Message-passing concurrency in LOB applications
LOB topology按ownership key划分:account/order/tenant/shard各有serial command stream;pricing或reporting可并行pure workers;portfolio risk按portfolio key聚合;gateway做authentication、routing和bounded ingress。跨owner workflow用saga/process manager维护步骤与compensation,不让一个“global actor”成为所有业务的锁。
gateway -> partition(accountId) -> account owner -> durable events
| |
v v
typed reply projections
process manager <- events -> commands to inventory/payment/shippingPartition key决定哪些invariants可强一致。转账跨两个accounts时,单owner无法原子修改双方;可用ledger owner、reservation protocol或database transaction,按domain requirements选。Rebalancing需迁移ownership并阻止双owners同时写,常用lease/epoch/fencing token。消息携带epoch,stale owner的write被store拒绝。
↡根据entity或tenant key把messages路由到唯一owner,同时保留扩容、hot-key和迁移规则的拓扑。切换account、pricing、risk与gateway
Observability and protocol evolution
Trace把command id、causation id、correlation id与entity/partition串起来;metrics记录enqueue rate、dequeue rate、queue age/depth、handler duration、restart、dead letter、timeout与dedup hits。不能把完整message payload默认写log,敏感LOB数据需要schema级redaction。Sampling也不能丢掉failure/dead-letter traces。
Message schema是跨版本contract。新增optional field有default,rename保留wire name,consumer先支持new/old,再producer切换。Mailbox中的old messages可能在部署后才处理,rolling upgrade尤其要验证。Golden messages做serialization/deserialization和semantic handler tests,不能只编译当前records。
Production gate: prove protocol under disruption
最终验收不止unit handler。Integration harness暂停consumer、填满mailbox、重复delivery、丢reply、crash after effect before checkpoint、restart与partition migration。每个scenario断言business invariant、dedup、reply classification、queue bound和recovery time。Chaos evidence需可复现,记录seed/schedule与message trace。
Shutdown先停止ingress,等待/期限内drain,checkpoint owner,未完成message按durable policy返回queue;不能进程退出时静默丢掉accepted command。Deployment compatible gate还要让old/new nodes交换messages,证明rolling window可用。
本章回顾:Message Passing把锁问题变成Protocol问题
- Single owner序列化private state,但cross-owner invariants仍需明确protocol。
- Ask timeout、cancellation与delivery status不是business commit事实,retry必须防重。
- Functional API隔离domain handler与agent runtime,pure transition便于重放和测试。
- Mailbox需要capacity、fairness、supervision、durability和schema evolution。
- LOB topology由ownership key和consistency boundary决定,并用fencing防止迁移双写。
练习
问题 1:怎样测试Reply丢失后的安全重试?
问题 2:Mailbox满时应选择哪种策略?
问题 3:Agent重启后怎样恢复State?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- single-owner state
- concurrency invariant boundary
- mailbox protocol
- agent API boundary
- ownership partition
原版目录概念补充核对
以下条目补齐官方目录中容易被示例主线掩盖的概念。它们不重复罗列目录,而是明确每项概念的机制、适用边界和验收证据。
Understanding message-passing concurrency:机制、边界与证据
Chapter 15. An introduction to message-passing concurrency中的Understanding message-passing concurrency应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。
Functional APIs, agent-based implementations:机制、边界与证据
Chapter 15. An introduction to message-passing concurrency中的Functional APIs, agent-based implementations应写成可组合的输入—输出契约,并把环境读取、状态改变与失败显式放在边界。用确定输入运行正常、空值和失败样本,同时记录返回值与副作用轨迹,证明结论不依赖隐藏状态。