<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Erlang on 止语Lab</title>
        <link>https://www.wujiachen.com.cn/tags/erlang/</link>
        <description>Recent content in Erlang on 止语Lab</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-cn</language>
        <lastBuildDate>Sat, 30 May 2026 17:57:01 +0800</lastBuildDate><atom:link href="https://www.wujiachen.com.cn/tags/erlang/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>并发模型三流派：CSP / Actor / 线程</title>
            <link>https://www.wujiachen.com.cn/posts/concurrency-models/</link>
            <pubDate>Sat, 30 May 2026 11:03:55 +0800</pubDate>
            <guid>https://www.wujiachen.com.cn/posts/concurrency-models/</guid>
            <description>&lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/cover.png&#34; alt=&#34;Featured image of post 并发模型三流派：CSP / Actor / 线程&#34; /&gt;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/cover.png&#34; alt=&#34;封面&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;很多并发模型比较，问的是&amp;quot;谁更先进&amp;quot;。这篇换个问法：状态归谁，等待归谁，失败归谁。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;很多文章讲并发模型，喜欢从一个大表开始：线程、协程、Actor、CSP、async/await，各自一列，优缺点排开。&lt;/p&gt;&#xA;&lt;p&gt;这种写法看起来完整，但读完很容易只记住几个标签：Go 是 CSP，Erlang 是 Actor，Java 是线程。&lt;/p&gt;&#xA;&lt;p&gt;问题就在这里。&lt;/p&gt;&#xA;&lt;p&gt;真实工程里，你不会因为一个模型&amp;quot;更先进&amp;quot;就选它。你真正关心的是：这段并发代码出了问题以后，我该去哪里看？谁能改状态？谁在等？谁接住失败？&lt;/p&gt;&#xA;&lt;p&gt;这篇不做先进性排名。但&amp;quot;不排名&amp;quot;不等于&amp;quot;三种模型完全等价&amp;quot;——某些场景客观偏好某种模型，电信级长连接和毫秒级故障转移下 Actor 有结构性优势。本文反对的是脱离场景的抽象排名，不是反对场景化判断。&lt;/p&gt;&#xA;&lt;p&gt;我会用同一个任务编排器，把 Go、Erlang、Java 三种心智模型放在同一张白板上看。不是看语法谁漂亮，而是看三件事：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&lt;strong&gt;状态归谁&lt;/strong&gt;：谁拥有结果，谁能修改，谁负责一致性。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;等待归谁&lt;/strong&gt;：阻塞、超时、取消、背压由谁表达。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;失败归谁&lt;/strong&gt;：一个子任务失败后，错误被关在哪里，谁决定恢复或扩散。&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;这三个问题，比&amp;quot;CSP / Actor / 线程谁好&amp;quot;更接近工程现场。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/opening-wrong-question-vs-three-questions.png&#34; alt=&#34;错误问题 vs 正确问题：从&amp;#34;谁更先进&amp;#34;转向&amp;#34;三问框架&amp;#34;&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;1-先固定一个任务聚合三个下游&#34;&gt;&lt;a href=&#34;#1-%e5%85%88%e5%9b%ba%e5%ae%9a%e4%b8%80%e4%b8%aa%e4%bb%bb%e5%8a%a1%e8%81%9a%e5%90%88%e4%b8%89%e4%b8%aa%e4%b8%8b%e6%b8%b8&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;1. 先固定一个任务：聚合三个下游&#xA;&lt;/h2&gt;&lt;p&gt;先别急着下定义。&lt;/p&gt;&#xA;&lt;p&gt;想象一个用户画像接口。它要同时请求三个下游：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;profile&lt;/code&gt;：基础资料，通常很快。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;billing&lt;/code&gt;：付费状态，偶尔返回业务错误。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;risk&lt;/code&gt;：风控标签，偶尔超时。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;聚合层要做的事也很普通：并发发起请求，等结果回来，组装成一个响应。如果某个下游超时，不能让整个请求无限挂着；如果某个下游明确失败，要决定其他任务还要不要继续跑。&lt;/p&gt;&#xA;&lt;p&gt;这个小场景写了三份最小实现：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;语言 / 风格&lt;/th&gt;&#xA;          &lt;th&gt;实现路径&lt;/th&gt;&#xA;          &lt;th&gt;运行场景&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Go / CSP 风格&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;goroutine + channel + context&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;success、timeout、worker-error&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Java / virtual thread&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;virtual thread + Future/CompletionService&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;success、timeout、worker-error&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Erlang / Actor 风格&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;process + message + link/EXIT&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;success、timeout、worker-error&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch1-task-orchestrator-scene.png&#34; alt=&#34;同一个任务编排器：三个下游、三种责任表达方式&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;代码不测性能。这里没有 QPS，也没有延迟排名。&lt;/p&gt;&#xA;&lt;p&gt;这么做是有意的。并发模型讨论很容易被性能数字带偏：哪个更快、哪个开销更低、哪个能撑更多连接。这些问题当然重要，但不是这篇要解决的事。此场景偏短生命周期聚合，Actor 的长生命周期优势未覆盖——边界先放这里。&lt;/p&gt;&#xA;&lt;p&gt;一个模型性能再好，如果团队不知道失败以后谁来收场，它还是会在生产环境里变成黑盒。反过来，一个模型看起来不够&amp;quot;酷&amp;quot;，但责任边界清楚，排障时反而更省命。&lt;/p&gt;&#xA;&lt;p&gt;技术选型最后比的，往往是事故发生时谁能最快定位、最快止血，谁能避免下一次复发。&lt;/p&gt;&#xA;&lt;p&gt;只看一件事：同一个工程任务，在不同模型里，责任被放到了哪里。&lt;/p&gt;&#xA;&lt;p&gt;并发模型最容易被讲成抽象概念。但工程师真正付出的成本，通常不在&amp;quot;能不能并发&amp;quot;，而在&amp;quot;并发以后谁负责收尾&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;下面进入三问。&lt;/p&gt;&#xA;&lt;h2 id=&#34;2-第一问状态归谁&#34;&gt;&lt;a href=&#34;#2-%e7%ac%ac%e4%b8%80%e9%97%ae%e7%8a%b6%e6%80%81%e5%bd%92%e8%b0%81&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;2. 第一问：状态归谁&#xA;&lt;/h2&gt;&lt;p&gt;先看状态。&lt;/p&gt;&#xA;&lt;p&gt;这个任务里，状态主要是&amp;quot;已经拿到了哪些下游结果&amp;quot;。它看起来只是一个结果集合，但并发一上来，问题就变了：多个任务能不能同时改它？如果能，谁保证一致性？如果不能，结果怎么汇合？&lt;/p&gt;&#xA;&lt;p&gt;Go 的写法很典型。&lt;/p&gt;&#xA;&lt;p&gt;Go 版本里，每个 worker goroutine 不直接修改聚合结果。它只把一个不可变的 &lt;code&gt;Result&lt;/code&gt; 发到 channel。真正持有结果列表的是聚合 goroutine——社区惯用 &lt;code&gt;errgroup&lt;/code&gt; 进一步封装这个模式。&lt;/p&gt;&#xA;&lt;p&gt;实验输出里，这个观察写成一句话：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景&lt;/th&gt;&#xA;          &lt;th&gt;状态所有权观察&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Go success / timeout / worker-error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;aggregator goroutine owns the result slice; workers only send immutable Result values&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;翻译成人话就是：worker 只交信封，不改账本。账本在聚合者手里。&lt;/p&gt;&#xA;&lt;p&gt;这就是 Go 偏 CSP 心智模型的好处。它鼓励你把协作关系写到通信结构里。channel 不只是传值，它也在暗示：谁能说话，谁能接收，谁能关闭这条路。&lt;/p&gt;&#xA;&lt;p&gt;但这里要补一条边界。&lt;/p&gt;&#xA;&lt;p&gt;Go 不是&amp;quot;无共享状态语言&amp;quot;。Go 项目里 mutex、atomic、WaitGroup、context 到处都是。把 Go 写成&amp;quot;只能 channel&amp;quot;是误导。更准确的说法是：Go 的默认心智模型鼓励你先问，能不能通过通信把状态所有权收束起来；如果收不住，再用锁和原子操作。&lt;/p&gt;&#xA;&lt;p&gt;Erlang 的状态边界更硬一点。&lt;/p&gt;&#xA;&lt;p&gt;在 Actor 风格里，每个 process 天然拥有自己的局部状态。外部不能直接伸手改它，只能发消息影响它。Erlang 实验里，worker process 完成后给 parent 发消息，parent 负责聚合。&lt;/p&gt;&#xA;&lt;p&gt;输出里是这样：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景&lt;/th&gt;&#xA;          &lt;th&gt;状态所有权观察&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Erlang success / timeout / worker-error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;each process owns its local state; parent aggregates only messages&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这个表达很 Actor：实体先存在，通信再发生。&lt;/p&gt;&#xA;&lt;p&gt;CSP 更像&amp;quot;我关心通道和协作关系&amp;quot;；Actor 更像&amp;quot;我关心实体和边界&amp;quot;。两者都是消息传递，但重心不同。CSP 的问题常常是&amp;quot;这条通信路径怎么设计&amp;quot;；Actor 的问题常常是&amp;quot;这个状态属于哪个实体&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;Java virtual thread 的位置又不一样。&lt;/p&gt;&#xA;&lt;p&gt;virtual thread 不会替你决定状态归谁。它解决的是阻塞线程的开销问题，共享状态如何保证一致性，仍然是应用层需要自己设计的。&lt;/p&gt;&#xA;&lt;p&gt;Java 实验里，聚合状态仍然是调用方普通对象和集合。virtual thread 让每个子任务可以继续写成同步阻塞代码，但它没有把共享状态变成私有状态，也不会自动给你 Actor 边界。&lt;/p&gt;&#xA;&lt;p&gt;输出里故意写得很直白：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景&lt;/th&gt;&#xA;          &lt;th&gt;状态所有权观察&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Java success / timeout / worker-error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;caller keeps aggregation state in ordinary objects; virtual threads do not change shared-state semantics&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这句话可以放进正文里反复提醒自己：virtual thread 改变了等待成本，但没有改变状态语义。&lt;/p&gt;&#xA;&lt;p&gt;第一问的答案大概是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Go 倾向于让状态通过通信汇聚。&lt;/li&gt;&#xA;&lt;li&gt;Erlang 倾向于让状态先属于一个 process。&lt;/li&gt;&#xA;&lt;li&gt;Java virtual thread 让你保留熟悉的对象和调用栈，但状态边界仍要自己设计。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;先别急着分高下。我们只看第一类责任：状态到底放在谁手里。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch2-state-ownership.png&#34; alt=&#34;状态所有权三分法：通道、实体、对象上下文&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;3-第二问等待归谁&#34;&gt;&lt;a href=&#34;#3-%e7%ac%ac%e4%ba%8c%e9%97%ae%e7%ad%89%e5%be%85%e5%bd%92%e8%b0%81&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;3. 第二问：等待归谁&#xA;&lt;/h2&gt;&lt;p&gt;并发代码第二个麻烦是等待。&lt;/p&gt;&#xA;&lt;p&gt;你发出三个下游请求，不可能一直等。&lt;code&gt;risk&lt;/code&gt; 慢了怎么办？整体超时谁说了算？已经成功的结果要不要保留？还没结束的任务谁去取消？&lt;/p&gt;&#xA;&lt;p&gt;先看边界感最强的方案。&lt;/p&gt;&#xA;&lt;p&gt;Erlang 的等待像 mailbox 里的时间边界。Erlang 版本用 parent process 收 worker 消息，&lt;code&gt;receive ... after&lt;/code&gt; 定义整体等待时间。&lt;code&gt;risk&lt;/code&gt; 超时后，parent 杀掉还没完成的 worker。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;erlang-timeout&lt;/code&gt; 的输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing,profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;timeout&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;Actor 风格的等待不是&amp;quot;共享一个 context&amp;quot;，而是&amp;quot;某个 process 在等消息&amp;quot;。这会改变排查入口。你会去看 mailbox、process 状态、消息有没有堆积，而不是先看某个共享取消对象。&lt;/p&gt;&#xA;&lt;p&gt;Go 的等待关系通常更显眼一些。&lt;/p&gt;&#xA;&lt;p&gt;在实验里，整体超时由 &lt;code&gt;context.WithTimeout&lt;/code&gt; 定义。worker 里用 &lt;code&gt;select&lt;/code&gt; 同时等两个东西：自己的模拟耗时，或者 &lt;code&gt;ctx.Done()&lt;/code&gt;。聚合者看到第一个错误后调用 &lt;code&gt;cancel()&lt;/code&gt;，兄弟 goroutine 通过同一个 context 收到取消。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;go-timeout&lt;/code&gt; 的输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;risk canceled: context deadline exceeded&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;你能从这组输出里看到 Go 的风格：等待和取消不是藏在运行时深处，而是直接写在应用编排代码里。&lt;/p&gt;&#xA;&lt;p&gt;这也是 Go 并发代码读起来有时&amp;quot;啰嗦&amp;quot;的原因。你要传 context，要处理 channel，要决定谁 close，要防止 goroutine 泄漏。它不替你消失这些问题。它只是把问题摆在桌面上。&lt;/p&gt;&#xA;&lt;p&gt;Java virtual thread 的等待最容易被误解。&lt;/p&gt;&#xA;&lt;p&gt;很多人听到 virtual thread，会以为 Java 的并发模型就变成了另一种东西。其实没有。virtual thread 的关键价值是：你可以继续写看起来同步的阻塞代码，而不会像平台线程那样为每个阻塞请求付出昂贵资源成本。&lt;/p&gt;&#xA;&lt;p&gt;Java 版本用 &lt;code&gt;Executors.newVirtualThreadPerTaskExecutor()&lt;/code&gt; 提交任务，再用 &lt;code&gt;CompletionService&lt;/code&gt; 等结果。代码仍然是 Future 编排：谁超时，谁取消，谁收错误，都由应用层决定。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;java-timeout&lt;/code&gt; 的输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing, profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;overall timeout after 70ms&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这说明 virtual thread 主要解决&amp;quot;等待能不能便宜一点&amp;quot;。至于等待语义怎么设计，还得回到应用编排。&lt;/p&gt;&#xA;&lt;p&gt;把这三种模型放在一起，第二问会变得很清楚：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Erlang 把等待写成 parent process 对 mailbox 的接收边界。&lt;/li&gt;&#xA;&lt;li&gt;Go 把等待和取消写成 &lt;code&gt;context/select/channel&lt;/code&gt; 的协作协议。&lt;/li&gt;&#xA;&lt;li&gt;Java virtual thread 把等待保留在同步调用栈里，让 JVM 承接更多阻塞成本，但取消策略仍在应用层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;不要只问&amp;quot;哪个模型更适合高并发&amp;quot;。先问：你的团队更想在代码哪里看到等待？process 边界里，通信结构里，还是同步调用栈里？&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch3-waiting-owner.png&#34; alt=&#34;等待责任三层图：mailbox、context、virtual thread&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;4-第三问失败归谁&#34;&gt;&lt;a href=&#34;#4-%e7%ac%ac%e4%b8%89%e9%97%ae%e5%a4%b1%e8%b4%a5%e5%bd%92%e8%b0%81&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;4. 第三问：失败归谁&#xA;&lt;/h2&gt;&lt;p&gt;状态和等待还只是开胃菜。并发模型真正拉开差距的地方，是失败。&lt;/p&gt;&#xA;&lt;p&gt;因为并发系统里，失败不是一个点。一个子任务失败以后，至少有三个问题跟着来：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;失败先被谁看到？&lt;/li&gt;&#xA;&lt;li&gt;兄弟任务要不要取消？&lt;/li&gt;&#xA;&lt;li&gt;这个失败会不会越过当前边界，影响更上层？&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;实验里加了两个失败场景：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;timeout&lt;/code&gt;：&lt;code&gt;risk&lt;/code&gt; 比整体超时更慢。&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;worker-error&lt;/code&gt;：&lt;code&gt;billing&lt;/code&gt; 明确返回错误。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Go 的 worker-error 输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;failed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;billing returned error&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这里的失败边界很应用化。&lt;code&gt;billing&lt;/code&gt; 出错后，聚合者调用 &lt;code&gt;cancel()&lt;/code&gt;，&lt;code&gt;risk&lt;/code&gt; 通过 context 退出。Go 没有一个&amp;quot;自动监督者&amp;quot;替你决定失败传播。你要把传播路径写出来。&lt;/p&gt;&#xA;&lt;p&gt;很多业务系统正需要这种显式控制。&lt;/p&gt;&#xA;&lt;p&gt;代价也在这里：如果你忘了传 context，忘了监听取消，忘了 drain channel，失败边界就会变成泄漏边界。Go 的并发事故里，goroutine 泄漏经常不是因为 goroutine 这个概念复杂，而是因为退出条件没有被完整表达。&lt;/p&gt;&#xA;&lt;p&gt;Erlang 的失败更像&amp;quot;边界先存在&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;最小 Erlang 实验里，worker 用 &lt;code&gt;spawn_link&lt;/code&gt; 启动——注意 parent 需要先 &lt;code&gt;process_flag(trap_exit, true)&lt;/code&gt; 才能把 EXIT 信号转为可处理消息，否则会被级联终止。&lt;code&gt;billing&lt;/code&gt; 失败时，parent 收到 &lt;code&gt;EXIT&lt;/code&gt; 信号，然后 kill 兄弟 process。&lt;/p&gt;&#xA;&lt;p&gt;输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;failed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;{task_error,billing}&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这当然还不是完整 OTP supervision。真正的 Erlang 工程会有 supervision tree、restart strategy、monitor 等更完整的设计。这里补一个关键区分：link 是双向的（一方死另一方跟着死），monitor 是单向的（只收通知，自己不死）。supervision tree 大量依赖 monitor 来实现&amp;quot;观察而不殉葬&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;但即使在这个最小实验里，你也能看到 Actor 心智模型的味道：失败是 process 边界上的信号，不只是一个返回值。一个实体死了，另一个实体可以观察到它的死亡，并决定下一步。&lt;/p&gt;&#xA;&lt;p&gt;Java virtual thread 的失败边界则更接近传统线程模型，只是线程变轻了。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;java-worker-error&lt;/code&gt; 的输出是：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;字段&lt;/th&gt;&#xA;          &lt;th&gt;输出&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;completed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[profile]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;failed&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[billing returned error]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;canceled&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[risk]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;error&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;billing returned error&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;这里的失败处理仍然靠应用编排 Future：捕获 &lt;code&gt;ExecutionException&lt;/code&gt;，记录第一个错误，取消未完成任务。&lt;/p&gt;&#xA;&lt;p&gt;Java 对&amp;quot;兄弟任务收束&amp;quot;这个问题已经给出了官方答案：&lt;code&gt;StructuredTaskScope&lt;/code&gt;（JEP 480，已正式进入 JDK）。它的设计思路是把一组子任务绑进同一个作用域，作用域退出时所有子任务必须结束——要么全成功，要么第一个失败触发其余取消。最小示例：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;try&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;StructuredTaskScope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;ShutdownOnFailure&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subtask&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;profile&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;fork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fetchProfile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;());&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subtask&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;billing&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;fork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fetchBilling&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;());&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Subtask&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;String&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;risk&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;fork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fetchRisk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;());&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;join&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;           &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 等待全部完成或第一个失败&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;scope&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;throwIfFailed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 有失败则抛出&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;profile&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;billing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;risk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;na&#34;&gt;get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;());&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// scope 关闭时，未完成的子任务被自动取消&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这比手动 Future 编排清晰不少——失败边界直接写在语法结构里。不过 &lt;code&gt;StructuredTaskScope&lt;/code&gt; 的 API 仍在演进（自定义策略、嵌套作用域等场景还有边界），它尚未覆盖 supervision tree 那种&amp;quot;长期实体+重启策略&amp;quot;的场景。&lt;/p&gt;&#xA;&lt;p&gt;到这里，第三问的差异就出来了：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Go：失败边界是应用协议，靠 context、channel、错误处理约定串起来。&lt;/li&gt;&#xA;&lt;li&gt;Erlang：失败边界是 process 语义的一部分，link/monitor/supervision 让失败可观察、可隔离、可恢复。&lt;/li&gt;&#xA;&lt;li&gt;Java：失败从手动 Future 编排走向 &lt;code&gt;StructuredTaskScope&lt;/code&gt; 的作用域收束；线程变轻，失败策略正在被语法化。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;当三种模型都有明确的责任位置时，争论&amp;quot;谁更先进&amp;quot;反而显得多余。真正的差异在于：失败信号沿着什么路径传播，传播到哪里停下来。&lt;/p&gt;&#xA;&lt;p&gt;Actor 的核心设计选择，是把状态和失败一起包进实体边界里。这让它在长生命周期、需要隔离和恢复的场景下有天然优势。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch4-failure-boundary.png&#34; alt=&#34;失败边界三路径：取消信号、EXIT 信号、Future 取消&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;5-一个辅助观察责任在代码表面的分布密度&#34;&gt;&lt;a href=&#34;#5-%e4%b8%80%e4%b8%aa%e8%be%85%e5%8a%a9%e8%a7%82%e5%af%9f%e8%b4%a3%e4%bb%bb%e5%9c%a8%e4%bb%a3%e7%a0%81%e8%a1%a8%e9%9d%a2%e7%9a%84%e5%88%86%e5%b8%83%e5%af%86%e5%ba%a6&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;5. 一个辅助观察：责任在代码表面的分布密度&#xA;&lt;/h2&gt;&lt;p&gt;为了避免只凭感觉，还做了一个粗略的辅助观察。&lt;/p&gt;&#xA;&lt;p&gt;方法很简单：对三份实验源码做关键词扫描，粗略统计&amp;quot;状态/聚合&amp;quot;&amp;ldquo;等待/调度&amp;quot;&amp;ldquo;失败/取消&amp;quot;三类责任关键词在代码里出现的相对密度。这只是一个粗略代理指标，不是性能比较，也不是可维护性评分——它只反映一个现象：代码表面上，你的注意力会被拉向哪个方向。&lt;/p&gt;&#xA;&lt;p&gt;观察结果：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Go 代码里，等待和取消关键词（context、cancel、select、Done）分布最密集——你很容易看到协作痕迹。&lt;/li&gt;&#xA;&lt;li&gt;Erlang 代码里，失败边界关键词（EXIT、link、kill、trap_exit）占比最高——process 边界和信号非常显眼。&lt;/li&gt;&#xA;&lt;li&gt;Java 代码里，三类关键词分布相对均匀，状态管理稍密——同步写法保留，取消仍在 Future 编排层。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;这个观察不能推出&amp;quot;Go 代码更复杂&amp;quot;或&amp;quot;Erlang 失败处理最好&amp;rdquo;。它能说明的只是：三种模型会把你注意力拉向不同地方。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;模型&lt;/th&gt;&#xA;          &lt;th&gt;写代码前先问&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Go&lt;/td&gt;&#xA;          &lt;td&gt;取消路径写完整了吗？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Java virtual thread&lt;/td&gt;&#xA;          &lt;td&gt;同步写法背后的失败收束写清楚了吗？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;Erlang / Actor&lt;/td&gt;&#xA;          &lt;td&gt;实体边界和观察关系设计清楚了吗？&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;模型不会替你收拾现场。它只是让某些责任更容易被看见，另一些责任必须靠你自己补上。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch5-responsibility-surface.png&#34; alt=&#34;责任显性化观察：状态、等待、失败三类信号在代码表面的位置&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;6-反例别把语言等同于模型&#34;&gt;&lt;a href=&#34;#6-%e5%8f%8d%e4%be%8b%e5%88%ab%e6%8a%8a%e8%af%ad%e8%a8%80%e7%ad%89%e5%90%8c%e4%ba%8e%e6%a8%a1%e5%9e%8b&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;6. 反例：别把语言等同于模型&#xA;&lt;/h2&gt;&lt;p&gt;专业读者可能已经想反驳了：&lt;/p&gt;&#xA;&lt;p&gt;Go 也能用锁啊。Java 也能写 Actor。Erlang 也不只是 mailbox。&lt;/p&gt;&#xA;&lt;p&gt;这些反驳都对。&lt;/p&gt;&#xA;&lt;p&gt;这篇文章从头到尾比较的是&amp;quot;默认心智模型&amp;rdquo;，不是&amp;quot;语言能力上限&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;语言像工具箱，默认心智模型像你最顺手拿起来的那把工具。Go 的工具箱里当然有锁和原子操作，但 goroutine、channel、context 会不断提醒你：协作关系能不能显式写出来？&lt;/p&gt;&#xA;&lt;p&gt;Java 的工具箱越来越大。virtual thread 让 thread-per-request 这种老写法重新变得可承受，但它并不会自动替你做状态隔离。你仍然要设计对象边界、取消策略、错误传播。&lt;/p&gt;&#xA;&lt;p&gt;Erlang 的工具箱也不只是&amp;quot;发消息&amp;quot;。如果只把 Erlang 当成 mailbox，你会错过它的核心设计：进程边界、失败信号、监督关系，以及&amp;quot;让它崩溃&amp;quot;背后的恢复语义。&lt;/p&gt;&#xA;&lt;p&gt;更准确的说法是：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Go 不是 CSP 的纯实现，但它偏向用通信结构组织并发。&lt;/li&gt;&#xA;&lt;li&gt;Erlang 不只是 Actor 消息队列，但它偏向用实体边界组织状态和失败。&lt;/li&gt;&#xA;&lt;li&gt;Java 不等于传统重线程；virtual thread 让同步线程模型在高并发 I/O 下重新有吸引力。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;一旦你接受这个边界，争论就会少很多。&lt;/p&gt;&#xA;&lt;p&gt;给语言贴标签已经够多了。真正要问的是：同一个问题，用哪套责任分配方式更容易写对、读懂、排查。&lt;/p&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch6-language-capability-vs-default-model.png&#34; alt=&#34;语言能力边界 vs 默认心智模型&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;7-最后给一张速查表&#34;&gt;&lt;a href=&#34;#7-%e6%9c%80%e5%90%8e%e7%bb%99%e4%b8%80%e5%bc%a0%e9%80%9f%e6%9f%a5%e8%a1%a8&#34; class=&#34;header-anchor&#34;&gt;&lt;/a&gt;7. 最后给一张速查表&#xA;&lt;/h2&gt;&lt;p&gt;如果你在设计一个并发模块，不要先问&amp;quot;我该用 CSP、Actor 还是线程&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;先问三句：&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;状态归谁？&lt;/li&gt;&#xA;&lt;li&gt;等待归谁？&lt;/li&gt;&#xA;&lt;li&gt;失败归谁？&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;然后再看场景。&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;场景信号&lt;/th&gt;&#xA;          &lt;th&gt;更该关注的问题&lt;/th&gt;&#xA;          &lt;th&gt;倾向的表达方式&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;多个 I/O 下游并发等待，取消关系复杂&lt;/td&gt;&#xA;          &lt;td&gt;等待归谁，取消怎么传播&lt;/td&gt;&#xA;          &lt;td&gt;Go channel/context，或 Java virtual thread + 明确 Future 编排&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;每个实体有长期状态和独立生命周期&lt;/td&gt;&#xA;          &lt;td&gt;状态归谁，失败被关在哪里&lt;/td&gt;&#xA;          &lt;td&gt;Actor/process 边界&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;团队强依赖同步调用栈可读性&lt;/td&gt;&#xA;          &lt;td&gt;能否保留顺序代码，同时降低等待成本&lt;/td&gt;&#xA;          &lt;td&gt;Java virtual thread&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;失败恢复比吞吐更重要&lt;/td&gt;&#xA;          &lt;td&gt;谁观察失败，谁负责重启/隔离&lt;/td&gt;&#xA;          &lt;td&gt;Erlang/Actor supervision 思路&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;状态集中且需要严格共享一致性&lt;/td&gt;&#xA;          &lt;td&gt;谁能改状态，锁或事务在哪里&lt;/td&gt;&#xA;          &lt;td&gt;线程 + 显式同步，或重新拆分状态所有权&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;&#xA;    &lt;img src=&#34;https://img.wujiachen.com.cn/concurrency-models/ch7-concurrency-model-cheatsheet.png&#34; alt=&#34;并发模型选型速查卡：先问谁负责&#34; loading=&#34;lazy&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;这张表不是标准答案。它只是把&amp;quot;模型选择&amp;quot;翻译成&amp;quot;责任选择&amp;quot;，让讨论别一上来就变成语言站队。&lt;/p&gt;&#xA;&lt;p&gt;下次看到并发模型争论时，先别急着站队。&lt;/p&gt;&#xA;&lt;p&gt;有人说 channel 优雅，你就问：状态归谁？&lt;/p&gt;&#xA;&lt;p&gt;有人说 Actor 才可靠，你就问：失败归谁？&lt;/p&gt;&#xA;&lt;p&gt;能把这三件事回答清楚，模型名字反而没那么神秘。&lt;/p&gt;&#xA;&lt;p&gt;CSP、Actor、线程的区别，在于把状态、等待和失败交给不同的角色负责。&lt;/p&gt;&#xA;&lt;p&gt;工程里最怕的，不是三种模型给出了不同答案。&lt;/p&gt;&#xA;&lt;p&gt;是这三个问题，从头到尾没人问过。&lt;/p&gt;&#xA;&lt;hr&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;原文发布于 &lt;a class=&#34;link&#34; href=&#34;https://www.wujiachen.com.cn/posts/concurrency-models&#34;  target=&#34;_blank&#34; rel=&#34;noopener&#34;&#xA;    &gt;止语Lab&lt;/a&gt;&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;</description>
        </item></channel>
</rss>
