异步并发模型的进化之路:从 Linux 内核进程到 Kotlin 协程,各平台的异步方式对比
先摆一张全景图
高 ↑ Linux 内核进程
安 │ ↕ fork + exec
全 │ JVM 进程
性 │ ↕
│ JVM 线程 (1:1 映射 OS 线程)
│ Node.js Worker Thread ↕ 轻量化 HarmonyOS Actor 线程
│ (V8 Isolate + 消息传递) ↕ (Actor 隔离 + 消息传递)
│ ↕
低 │ Kotlin 协程 (用户态调度, 挂起不阻塞线程)
└──────────────────────────────────────────────────────────→
低 ← 创建/切换开销 → 高
核心规律:越往上越安全(隔离越强),但越重(创建/切换越慢);越往下越轻,但越不安全(越需要开发者自己保证正确性)。
第一层:Linux 内核进程 —— 最重的"安全堡垒"
它是什么
操作系统进行资源分配和隔离的基本单位。每个进程拥有独立的虚拟地址空间、文件描述符表、信号处理器、内核栈。在 Linux 中,进程和线程本质上都是 task_struct,区别只在于 clone() 时传的 flags。
怎么调度
用户代码 → 系统调用/中断 → 内核态 → schedule() → context_switch()
├── switch_mm() (切换页表, 刷新 TLB) ← 最贵的部分
└── switch_to() (切换寄存器/栈)
调度器:CFS(完全公平调度器),按 vruntime 分配 CPU 时间
抢占式:时间片用完或更高优先级任务就绪时,内核强制切换
切换代价:必须刷新 TLB(Translation Lookaside Buffer),后续内存访问全 cache miss
内存模型
进程A 虚拟地址空间 进程B 虚拟地址空间
┌─────────────────┐ ┌─────────────────┐
│ 栈│ 堆│数据│代码 │ │栈 │堆 │数据│代码 │
└──┬──────────────┘ └──┬──────────────┘
│ MMU 页表 │ MMU 页表
└────────┬────────────────────┘
↓
物理内存 (页框)
每个进程有独立的页表,MMU(内存管理单元)在硬件层面强制隔离——进程 A 永远不可能访问进程 B 的内存。
通信方式
创建开销
一句话评价
最安全,但最重。 隔离是硬件级的,一个进程崩溃不会影响其他。但创建和切换开销大,不适合高频创建销毁。适合对安全要求极高的场景。
什么场景用它
✅ Chrome 多进程架构(每个 Tab 独立进程,一个崩了不拖垮整个浏览器)
✅ 数据库服务(PostgreSQL 的 fork 模型,每个连接一个进程)
✅ Nginx Worker(pre-fork 模型,一次性 fork,后续复用)
❌ 高频创建销毁(不可能每秒 fork 1000 个进程)
❌ 频繁通信(IPC 开销比线程间通信大得多)
第二层:JVM 进程 —— 在 OS 进程之上再加一层
它是什么
JVM 进程本质上就是一个 Linux 进程,但它在这个进程内部又构建了一整套抽象:字节码执行引擎、JIT 编译器、GC、类加载器、JMM(Java 内存模型)。
┌─────────────────────────────────────────────┐
│ JVM 进程 │
│ ┌───────────────────────────────────────┐ │
│ │ Java 堆 (Heap) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ 对象 │ │ 对象 │ │ 对象 │ │ 对象 │ │ │ ← 所有线程共享
│ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ └───────────────────────────────────────┘ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 线程1栈 │ │ 线程2栈 │ │ 线程3栈 │ ... │ ← 每线程私有
│ │ ~1MB │ │ ~1MB │ │ ~1MB │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ┌───────────────────────────────────────┐ │
│ │ 方法区 / Metaspace (类元数据) │ │
│ └───────────────────────────────────────┘ │
│ ┌───────────────────────────────────────┐ │
│ │ JIT 编译的本地代码缓存 │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
和 Linux 原生进程的关键差异
一句话评价
JVM 进程 = Linux 原生进程 + 一套"进程内的操作系统"。它给了你线程、内存自动管理、跨平台字节码,但代价是额外的内存和启动开销。
第三层:JVM 线程 —— 进程内的并行执行流
它是什么
JVM 线程是 1:1 映射到 OS 内核线程的。每个 new Thread() 都会创建一个真实的 OS 线程。这是"进程 → 线程"的轻量化跳跃——多个线程共享进程的地址空间,省掉了 fork 时复制页表的开销。
怎么调度
JVM 线程 = OS 线程 (Linux 中就是带 CLONE_VM 的 task_struct)
│
├── 共享:堆、方法区、打开的文件、信号处理
├── 独有:栈 (~1MB)、程序计数器、线程局部存储 (TLS)
│
└── 调度:内核 CFS 调度器 (同 Linux 进程),抢占式
内存模型(JMM)
主内存 (Main Memory) — 物理上就是堆
│
├── 线程1 工作内存 ──→ 缓存 + 寄存器 (CPU 核心1)
├── 线程2 工作内存 ──→ 缓存 + 寄存器 (CPU 核心2)
└── 线程3 工作内存 ──→ 缓存 + 寄存器 (CPU 核心3)
问题:同一变量在不同核心的缓存中可能不同!
解决:volatile / synchronized / Lock / Atomic
创建开销
核心痛点
┌─────────────────────────────────────────────┐
│ 线程共享内存带来的"三大恶魔" │
│ │
│ 1. 竞态条件 (Race Condition) │
│ count++ 不是原子的:读→改→写 之间 │
│ 另一个线程可能插进来 │
│ │
│ 2. 死锁 (Deadlock) │
│ 线程A: lock(L1) → lock(L2) │
│ 线程B: lock(L2) → lock(L1) │
│ 互相等待 → 永远卡死 │
│ │
│ 3. 内存可见性 (Visibility) │
│ CPU 缓存 + 指令重排 → │
│ 线程 A 写了变量,线程 B 可能看不到 │
└─────────────────────────────────────────────┘
一句话评价
进程的"轻量化版本"——共享地址空间省掉了最贵的 fork 开销,但引入了并发编程三大恶魔:竞态、死锁、可见性。 是所有高层抽象(协程、Actor)要"解决"的对象。
什么场景用它
✅ 通用并发计算(CPU 密集型并行)
✅ 需要共享大量内存数据的场景(避免序列化开销)
✅ 小规模并发(几十到几百个线程)
❌ 大规模并发(成千上万个线程 → OOM)
❌ 开发者不想手动管理锁的场景
第四层:Node.js 线程模型 —— 单线程事件循环 + 线程池 + Worker Threads
它是"几层"?
Node.js 的线程模型不是单一抽象,而是一个三层复合体:
┌─────────────────────────────────────────────────────────────┐
│ Node.js 进程 │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ 主线程:Event Loop (单线程) │ │
│ │ ┌──────────────────────────────────────────────┐ │ │
│ │ │timers → pending → idle → poll → check → close│ │ │
│ │ │ ↑ epoll_wait (epoll/kqueue/IOCP) │ │ │
│ │ └──────────────────────────────────────────────┘ │ │
│ │ 只跑 JS!不阻塞!非阻塞 I/O 由内核完成! │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ libuv 线程池 (默认 4 线程, 可配到 1024) │ │
│ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │
│ │ │ 线程1│ │ 线程2 │ │ 线程3│ │ 线程4│ │ │
│ │ │ fs │ │crypto│ │ zlib │ │ dns │ │ │
│ │ └──────┘ └──────┘ └──────┘ └──────┘ │ │
│ │ 只跑 C++ 任务!不跑 JS! │ │
│ └────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Worker Threads (手动创建) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ Worker 1 │ │ Worker 2 │ │ Worker 3 │ │ │
│ │ │ 独立 V8 │ │ 独立 V8 │ │ 独立 V8 │ │ │
│ │ │ 独立 Loop│ │ 独立 Loop │ │ 独立 Loop│ │ │
│ │ │ ~5MB │ │ ~5MB │ │ ~5MB │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ ← MessageChannel / SharedArrayBuffer → │ │
│ │ 真正跑 JS 的多线程! │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
主线程 Event Loop:单线程的"协程化思维"
Node.js 主线程是一个单线程 + 非阻塞 I/O + 事件驱动的模型。JavaScript 代码始终在同一个线程、同一个调用栈中执行。
关键概念:Event Loop 不等于"单线程"
主线程 JS 代码 = 单线程(只有一个 V8 Isolate 执行 JS)
libuv 线程池 = 多线程(4 个 C++ 线程处理 fs/crypto/zlib/dns)
Worker Threads = 多 V8 Isolate(真正跑 JS 的多线程)
"Node.js 是单线程的" —— 这句话只说对了一半。
准确的说法:
Node.js 的 JavaScript 执行是单线程的,但 Node.js 进程不是。
Event Loop 六大阶段
` ┌───────────────────────────┐
┌▶│ ① timers │ setTimeout / setInterval
│ └─────────────┬─────────────┘
│ ┌─────────────▼─────────────┐
│ │ ② pending callbacks │ 延迟的 I/O 错误回调
│ └─────────────┬─────────────┘
│ ┌─────────────▼─────────────┐
│ │ ③ idle, prepare │ 内部使用
│ └─────────────┬─────────────┘
│ ┌─────────────▼─────────────┐ ┌──────────────┐
│ │ ④ poll │◀─ ──│ epoll_wait │ ← 核心!
│ └─────────────┬─────────────┘ └──────────────┘ 阻塞在这等 I/O
│ ┌─────────────▼─────────────┐
│ │ ⑤ check │ setImmediate()
│ └─────────────┬─────────────┘
│ ┌─────────────▼─────────────┐
└──│ ⑥ close callbacks │ socket.on('close')
└───────────────────────────┘
每次回调执行完后:
① 清空 process.nextTick 队列
② 清空 Promise microtask 队列
③ 才进入下一个阶段
libuv 线程池
为什么需要线程池?
epoll/kqueue/IOCP 只能做网络 I/O 的非阻塞 ——
但文件系统、DNS 解析、加密运算这些没有真正的异步 OS 接口(io_uring 之前)。
所以 libuv 把这些操作扔给线程池:
fs.readFile() → 线程池线程阻塞读 → 完成后回调主线程
crypto.pbkdf2() → 线程池线程 CPU 计算 → 完成后回调主线程
dns.lookup() → 线程池线程 getaddrinfo → 完成后回调主线程
默认 4 个线程。如果你同时发 100 个 pbkdf2:
→ 前 4 个在执行,后 96 个排队 → libuv 线程池饥饿
→ 所有 fs 操作也被堵住!
Worker Threads:真正的 JS 多线程
// main.js
const { Worker } = require('worker_threads');
const worker = new Worker('./heavy-task.js');
worker.on('message', (result) => console.log('结果:', result));
worker.postMessage({ data: largeArray });
// heavy-task.js
const { parentPort } = require('worker_threads');
parentPort.on('message', (msg) => {
const result = cpuHeavyCompute(msg.data); // 真正的并行 JS!
parentPort.postMessage(result);
});
每个 Worker Thread 有自己独立的:
V8 Isolate(独立 JS 执行环境)
Event Loop(独立的事件轮询)
堆内存(独立的 GC 管理)
~5–10 MB 的初始内存占用
通信方式:
核心数据
一句话评价
Node.js 的线程模型是"分层解耦"的典范:主线程 Event Loop 用单线程 + 非阻塞 I/O 搞定高并发网络请求(C10K/C100K),libuv 线程池偷偷把阻塞操作异步化,Worker Threads 给真正需要 CPU 并行的场景一条出路。它的设计哲学是"默认事件驱动,按需加线程",而非像 JVM 那样"每个任务一个线程"。
什么场景用它
✅ 高并发 I/O(WebSocket 长连接、API 网关、实时消息)
✅ CPU 密集型 Node.js 任务(Worker Threads + 共享内存)
✅ 微服务 / Serverless(启动快 5ms,冷启动优势巨大)
✅ 全栈 JavaScript(前后端一套语言)
❌ 机器学习训练(不如 Python/C++ 生态)
❌ 大规模共享内存并行计算(SharedArrayBuffer 有,但心智负担高)
第五层:HarmonyOS Actor 线程 —— 给线程加回进程的隔离
它是什么
HarmonyOS 的并发模型基于 Actor 模式。还记得第一层 Linux 进程的"内存隔离 + 消息通信"吗?Actor 线程在线程这个轻量级别上重现了这种设计——每个 Actor 线程有自己独立的内存空间,线程间不能直接共享内存,必须通过消息传递通信。
架构
┌─────────────────────────────────────────────────────────────┐
│ ArkRuntime 进程 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Actor 1 │ │ Actor 2 │ │ Actor 3 │ (每个是一个线程) │
│ │ 独立内存 │ │ 独立内存 │ │ 独立内存 │ │
│ │ 独立状态 │ │ 独立状态 │ │ 独立状态 │ │
│ │ 独立 Loop│ │ 独立 Loop│ │ 独立 Loop│ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ │ │
│ postMessage 消息序列化 │
│ (不能共享内存!) │
│ │
│ 对比传统线程: │
│ 传统线程 ─ 共享堆 ─ 需锁 ─ 有竞态/死锁 │
│ Actor ─ 隔离堆 ─ 无锁 ─ 无竞态/死锁 │
└─────────────────────────────────────────────────────────────┘
TaskPool:系统托管的线程池
import { taskpool } from '@kit.ArkTS';
@Concurrent
function compute(data: number): number {
let sum = 0;
for (let i = 0; i < data; i++) sum += Math.sqrt(i);
return sum;
}
// 一行调用,系统自动调度
const result = await taskpool.execute(compute, 10_000_000);
// 支持优先级、取消、TaskGroup 并行
内部机制:
自动扩缩容(默认最大线程数 = CPU 核心数 - 1)
支持优先级调度(HIGH/NORMAL/LOW)+ 防饥饿
任务执行上限 3 分钟(超时自动回收)
支持
TaskGroup批量提交(类似Promise.all)
Worker:手动管理的常驻线程
import { worker } from '@kit.ArkTS';
// 主线程
const w = new worker.ThreadWorker('entry/ets/workers/MyWorker.ts');
w.postMessage({ type: 'start', data: largeDataset });
w.onmessage = (e) => console.log('Worker 结果:', e.data);
// 用完记得 terminate()
// MyWorker.ts
worker.workerPort.onmessage = (e) => {
const result = heavyProcessing(e.data);
worker.workerPort.postMessage(result);
};
Sendable:受控的共享内存(打破 Actor 不能共享的限制)
import { ArkTSUtils, collections } from '@kit.ArkTS';
@Sendable // ← 对象放在共享堆 SharedHeap
class SharedData {
@Trace name: string = '';
lock_: ArkTSUtils.locks.AsyncLock = new ArkTSUtils.locks.AsyncLock();
}
// 跨线程引用传递,零拷贝!
// 但需要 AsyncLock 保护并发修改
核心数据
和 Linux 进程的相似性
这是理解 Actor 模型最巧妙的切入点:
Linux 进程:内存隔离 ← MMU 硬件强制 → 通过 IPC 消息通信
Actor 线程:内存隔离 ← 运行时软约束 → 通过 postMessage 通信
Actor 线程 ≈ 在进程内部用线程实现的"轻量级进程"
它把进程的"隔离安全性"和线程的"轻量高效"焊接在了一起:
比进程快(创建 μs vs ms),比传统线程安全(无锁 vs 有锁)
一句话评价
Actor 模型干了件反直觉的事——在"线程越来越轻、共享越来越容易"的趋势下,它反而把"共享"禁掉了。 这是一次"用性能换安全"的权衡:消息序列化有开销,但换来零竞态零死锁。当业务真的需要共享时,再用 Sendable + AsyncLock 按需开启。设计哲学:"默认不共享(安全),按需共享(你要负责)"。
什么场景用它
✅ 需要故障隔离的场景(一个 Actor 崩了不影响其他)
✅ 分布式/多核并行的 HarmonyOS 应用
✅ 需要安全的并发(开发者不想管锁)
❌ 需要共享 500MB 缓存(序列化开销不可接受,Sendable 也重)
❌ 高频细粒度数据交换(消息序列化开销大于操作本身)
第六层:Kotlin 协程 —— 线程内部的"微任务"
它是什么
协程是运行在线程之上的、用户态的、协作式的轻量级并发单元。它不创建新线程,而是在现有线程上"插入"可挂起的执行片段。本质上是一套编译期的状态机 + 运行时的调度器。
怎么实现的(编译期魔法)
// 你写的代码
suspend fun fetchUser(id: String): User {
val token = getToken() // 挂起点 1
val user = api.getUser(token) // 挂起点 2
return user
}
// 编译器把它变成 ↓
// (简化示意,实际是 Continuation + label 状态机)
fun fetchUser(id: String, continuation: Continuation<User>): Any? {
when (continuation.label) {
0 -> {
continuation.label = 1
val result = getToken(continuation)
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
// 没挂起,继续执行
}
1 -> {
continuation.label = 2
val token = continuation.result as Token
val result = api.getUser(token, continuation)
if (result == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED
}
2 -> {
return continuation.result as User
}
}
}
关键优化:如果被调函数没有真正挂起(比如缓存命中、内存数据),就不会走 suspend 流程,而是同步返回——零开销抽象。
怎么调度(运行时)
┌────────────────────────────────────────────────┐
│ Dispatchers.Default (CPU 密集型) │
│ ┌──────────────────────────────────────────┐ │
│ │ 共享线程池 (大小 = CPU 核心数) │ │
│ │ Thread1 ◄── 协程A, 协程B, 协程C, ... │ │
│ │ Thread2 ◄── 协程D, 协程E, 协程F, ... │ │
│ └──────────────────────────────────────────┘ │
│ │
│ Dispatchers.IO (I/O 密集型) │
│ ┌──────────────────────────────────────────┐ │
│ │ 弹性线程池 (按需创建,默认上限 64) │ │
│ │ 协程阻塞 → 线程可去跑其他协程 │ │
│ └──────────────────────────────────────────┘ │
│ │
│ Dispatchers.Main (UI 线程) │
│ ┌──────────────────────────────────────────┐ │
│ │ 单线程 ← 所有 UI 更新必须在这里 │ │
│ └──────────────────────────────────────────┘ │
└────────────────────────────────────────────────┘
核心:一个线程可以跑成千上万个协程
协程挂起 ≠ 线程阻塞
线程被释放去跑别的协程
核心数据
和 Node.js Event Loop 的对比
一个有意思的对比:
设计哲学相似:都是"单线程调度 + 异步非阻塞",但 Kotlin 协程的 suspend 比 JS 的 async/await 更底层(编译期状态机 vs 运行时 Promise),且 Kotlin 可以在多个线程池之间灵活切换。
一句实话:协程不是银弹
协程的最优场景是 I/O 密集型——大部分时间在等网络/磁盘响应,协程在这期间不占线程,所以能几十万个同时跑。但在 CPU 密集型场景(纯计算),协程的优势消失——它只是把 CPU 计算分配到线程池的线程上,并没有让计算本身变快,此时和直接用线程池没本质区别。
一句话评价
协程是目前最轻量的并发抽象——不是因为它"做了很多",恰恰是因为它"几乎什么都没做"。 它只是把编译器和运行时的现有能力(状态机 + 线程池)重新编排了一下,让你用同步的写法享受异步的性能。代价是:函数染色(suspend 会传染)、调试栈难读、不适合纯 CPU 计算。
什么场景用它
✅ 高并发网络请求(几十万个连接同时等待响应)
✅ Android/Kotlin 后端微服务(结构化并发)
✅ 数据库查询并发(async + await 并行请求)
✅ 需要简洁代码风格(同步写法异步执行)
❌ 纯 CPU 计算(用协程没优势,直接用线程池)
❌ 简单同步程序(引入协程增加复杂度)
全方位对比表
层次包含关系:一张图看清所有层级
┌──────────────────────────────────────────────────────────────────────┐
│ Linux 内核进程 (OS 级) │
│ 独立的虚拟地址空间 / MMU 硬件隔离 / 内核调度 / fork+exec 创建 │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ JVM 进程 (OS 进程 + 运行时) │ │
│ │ 在 OS 进程内再加一层:JMM、GC、JIT、字节码执行 │ │
│ │ ┌──────────────────────────────────────────────────────────┐ │ │
│ │ │ JVM 线程 (1:1 映射 OS 线程) │ │ │
│ │ │ 共享堆,需锁,有竞态/死锁/可见性问题 │ │ │
│ │ │ ┌────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ Kotlin 协程 (用户态) │ │ │ │
│ │ │ │ 挂起不阻塞线程 / 编译期状态机 / 纳秒级创建 │ │ │ │
│ │ │ │ 一个线程上可以跑成千上万个协程 │ │ │ │
│ │ │ └────────────────────────────────────────────────────┘ │ │ │
│ │ └──────────────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ Node.js 进程 (OS 进程 + V8) │ │
│ │ ┌─────────────────────┐ ┌───────────────────────────────┐ │ │
│ │ │ Event Loop (单线程) │ │ Worker Threads (多 V8 Isolate) │ │ │
│ │ │ 非阻塞 I/O + libuv │ │ 独立堆 + 独立 Loop │ │ │
│ │ │ C10K 轻松应对 │ │ CPU 并行 + SharedArrayBuffer │ │ │
│ │ └─────────────────────┘ └───────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ │ HarmonyOS ArkRuntime 进程 │ │
│ │ ┌─────────────────────┐ ┌───────────────────────────────┐ │ │
│ │ │ TaskPool (自动管理) │ │ Worker (手动管理) │ │ │
│ │ │ 优先级 + 取消 │ │ 长驻后台 + 状态保持 │ │ │
│ │ │ 线程池弹性扩缩容 │ │ Actor 内存隔离 │ │ │
│ │ └─────────────────────┘ └───────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
每一层的"轻量化"本质:
进程 → 线程: 共享地址空间 (省掉了 fork 的页表复制)
线程 → 协程: 共享线程 (省掉了内核调度和栈分配)
线程 → Actor 线程: 加回隔离 (安全) 但保留线程的轻量 (效率)
线程 → Event Loop: 彻底不用多线程 (省掉了锁 + 上下文切换)
硬币的另一面:没有免费的进化
每一层"进化"都解决了上层的问题,但也都付出了代价。没有一种方案在所有维度上都优于另一种。
进程 ──────────→ 线程
解决:太重、创建慢
代价:放弃了内存隔离 → 竞态/死锁/可见性/Heisenbug
最佳:通用并行计算
最差:安全第一的场景
线程 ──────────→ 协程
解决:线程资源浪费、并发数受限、异步回调地狱
代价:放弃抢占式调度、函数染色、调试困难、不适合 CPU 密集
最佳:高并发 I/O
最差:纯 CPU 计算
线程 ──────────→ Actor 线程
解决:锁、死锁、竞态条件
代价:放弃共享内存便利性、消息序列化开销
最佳:分布式、故障隔离场景
最差:需要共享大内存、高频细粒度交互
线程 ──────────→ Node.js Event Loop
解决:多线程的锁、上下文切换开销、callback 嵌套
代价:单线程阻塞会影响所有请求、不适合 CPU 密集
最佳:高并发 I/O (WebSocket、API 网关)
最差:CPU 密集型计算 (需要 Worker Threads 补偿)
反直觉的例子:全局计数器
场景:实现一个全局计数器,被 100 个并发任务频繁读写
这个场景的赢家是 Node.js 单线程方案(无需任何同步机制)和 JVM AtomicInteger(共享内存优势)——最"现代"的 Actor 反而最慢。
选型决策:什么时候用谁
你需要什么样的并发?
安全第一(一个崩了不能影响其他)?
└── 进程 (Linux / JVM) — MMU 硬件隔离是最强的
├── 短生命周期任务 → 进程池 (如 Nginx pre-fork)
└── 长生命周期 + 高性能 → 看看 Actor
通用后端服务(有共享数据状态)?
└── JVM 线程 (Spring / Kotlin 后端) — 共享内存就是刚需
└── 高并发 I/O 密集?→ 加 Kotlin 协程,不换模型
高并发 I/O(WebSocket、API 网关、实时消息)?
└── Node.js Event Loop — C10K 的经典方案
└── 有 CPU 密集部分?→ Worker Threads 补偿
安全 + 隔离 + 分布式(移动端/嵌入式/微服务)?
└── HarmonyOS Actor — 进程的安全 + 线程的效率
└── 需要共享数据?→ Sendable + AsyncLock(有代价)
极致高并发(几十万个连接,每个等待时间很长)?
└── Kotlin 协程 / Go Goroutine — 纳秒创建 + 用户态调度
└── CPU 密集部分?→ 协程扔到 Dispatchers.Default + 线程池
既要高并发 I/O 又要 CPU 并行?
└── 混合架构:Node.js Event Loop (I/O) + Worker Threads (CPU)
或 Kotlin协程 (I/O) + Dispatchers.Default (CPU)
或 Go Goroutine (I/O) + 手动 runtime.GOMAXPROCS (CPU)
总结:好工程师脑子里有什么
这篇文章讲了六种并发模型,但比记住它们的差异更重要的,是建立一种思维方式:
1. 每个模型都有"设计时针对的场景"和"设计时放弃的东西"
2. "更现代"不代表"更好"
协程比线程新,Actor 比协程新,但全局计数器的例子告诉我们——在特定场景下,最古老的 JVM 线程 + AtomicInteger 反而是最优解。
3. 真正的工程能力 = 知道每种方案的"短板"
面试中能背出协程的优势,是及格分。
能说清楚"协程不适合什么场景、为什么",才是高分回答。
4. 一句话记住每种模型
Linux 进程 — 最重的安全堡垒 (硬件隔离, 故障隔离, 毫秒级创建)
JVM 进程 — 在 OS 进程上加了一个"进程内的操作系统"
JVM 线程 — 轻量化的进程 (共享堆, 需要锁, 有三大恶魔)
Node.js EL — 单线程搞定高并发 I/O, 不是"单线程"而是"聪明地分配线程"
Node.js Worker — JS 的多线程答案 (独立 V8 Isolate, 消息传递, 共享内存)
Actor 线程 — 给线程穿上了进程的铠甲 (隔离 + 消息, 无锁)
Kotlin 协程 — 线程内的微任务 (纳秒创建, 挂起不阻塞, 十万级并发)
本文是一次从底层到上层的并发模型梳理。写它的初衷是理解 HarmonyOS Actor 模型时发现——如果不理解 Linux 进程和 JVM 线程,就不可能真正理解 Actor 为什么要设计成这样;如果不理解协程,也不可能理解 Actor 的代价到底在哪。
微信
支付宝