先摆一张全景图

高 ↑                            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 的内存

通信方式

方式

机制

延迟

管道 (pipe)

内核缓冲区,单向

微秒级

消息队列

内核维护的消息链表

微秒级

共享内存

映射同一物理页(最快的 IPC)

纳秒级(纯内存)

Socket

网络栈,支持跨主机

毫秒级

创建开销

场景

耗时

fork() 小进程

60–170 μs

fork() 4GB 地址空间

~20,000 μs(约 120 倍)

fork() + exec() 执行简单程序

~700 μs

最小内存占用

~2 MB(仅栈 + 代码段)

典型应用占用

100 MB+

一句话评价

最安全,但最重。 隔离是硬件级的,一个进程崩溃不会影响其他。但创建和切换开销大,不适合高频创建销毁。适合对安全要求极高的场景。

什么场景用它

✅ 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 原生进程的关键差异

维度

Linux 原生进程

JVM 进程

内存模型

OS 虚拟地址空间

JMM(主内存 + 工作内存,happens-before)

"进程"内并发

没有(就一个执行流)

有,通过 JVM 线程

内存管理

OS 页表 + malloc/free

GC(G1/ZGC/Shenandoah)自动回收

代码执行

CPU 直接执行机器码

解释执行 → JIT 编译为机器码

启动开销

fork 后 exec

额外:JVM 启动 (~100ms) + 类加载 + JIT 预热

典型内存占用

进程本身的内存

额外 +200 MB~ (堆默认值 + JVM 自身)

一句话评价

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
​

创建开销

指标

数据

创建耗时

~10 μs(系统调用clone

栈内存

默认 1 MB/线程(-Xss 可调)

上下文切换

~1–10 μs(不换页表时极快,换页表时需清 TLB)

最大并发数

数千(受栈内存限制)

10,000 线程的栈内存

~10 GB → OOM

核心痛点

┌─────────────────────────────────────────────┐
│          线程共享内存带来的"三大恶魔"         │
│                                             │
│  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 的初始内存占用

通信方式

方式

机制

开销

postMessage(默认)

结构化克隆(深拷贝)

高(大数据慢)

TransferList

转移 ArrayBuffer 所有权

零拷贝(原线程失效)

SharedArrayBuffer + Atomics

真正的共享内存

零拷贝(需原子操作)

MessageChannel

双向专用通道

同上

核心数据

指标

数据

Worker 启动时间

~5 ms(vs JVM 线程 ~10 μs,vs 进程 ~100ms)

Worker 内存

~5–10 MB(vs JVM 线程 ~1 MB 初始栈)

最大 Worker 数

数百个(受 V8 Isolate 开销限制)

10,000 个网络连接

Event Loop 单线程处理,~48 MB 内存

同条件下 JVM 线程

OOM(10,000 × 1MB = 10GB)

CPU 密集型加速比

4 核 8 线程 → ~3.5x(接近线性)

一句话评价

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 保护并发修改
​

核心数据

指标

数据

Actor 创建开销

微秒级(线程池复用)

内存占用

~1–2 MB / Actor

Worker 最大数

64 个(API 12+)

TaskPool 最大线程

核心数 - 1(弹性扩缩)

Sendable 引用传递性能

1MB 数据比序列化快~100 倍

通信机制

消息序列化(默认)/ Sendable 引用传递(高级)

和 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 更新必须在这里            │  │
│  └──────────────────────────────────────────┘  │
└────────────────────────────────────────────────┘

核心:一个线程可以跑成千上万个协程
      协程挂起 ≠ 线程阻塞
      线程被释放去跑别的协程
​

核心数据

指标

Kotlin 协程

JVM 线程

差距

创建开销

纳秒级(对象分配)

~10 μs

~10,000×

内存占用

~100 字节

~1 MB 栈

~10,000×

最大并发数

100,000+

~数千

~100×

上下文切换

用户态函数调用

内核态 + TLB

~100×

挂起/恢复

状态机跳转

保存/恢复寄存器 + 栈

~1000×

和 Node.js Event Loop 的对比

一个有意思的对比:

Node.js Event Loop

Kotlin 协程

单线程跑多任务

✅ 回调/Promise

✅ 挂起/恢复

阻塞 I/O 处理

libuv 线程池

withContext(IO) 切换线程池

CPU 密集型

Worker Threads

Dispatchers.Default

"不要阻塞"

不要阻塞 Event Loop

不要阻塞底层线程

语法风格

async/await(ES2017)

suspend/await(原生)

并发上限

受 Event Loop 吞吐限制

受内存限制(可创建几十万)

设计哲学相似:都是"单线程调度 + 异步非阻塞",但 Kotlin 协程的 suspend 比 JS 的 async/await 更底层(编译期状态机 vs 运行时 Promise),且 Kotlin 可以在多个线程池之间灵活切换。

一句实话:协程不是银弹

协程的最优场景是 I/O 密集型——大部分时间在等网络/磁盘响应,协程在这期间不占线程,所以能几十万个同时跑。但在 CPU 密集型场景(纯计算),协程的优势消失——它只是把 CPU 计算分配到线程池的线程上,并没有让计算本身变快,此时和直接用线程池没本质区别。

一句话评价

协程是目前最轻量的并发抽象——不是因为它"做了很多",恰恰是因为它"几乎什么都没做"。 它只是把编译器和运行时的现有能力(状态机 + 线程池)重新编排了一下,让你用同步的写法享受异步的性能。代价是:函数染色(suspend 会传染)、调试栈难读、不适合纯 CPU 计算。

什么场景用它

✅ 高并发网络请求(几十万个连接同时等待响应)
✅ Android/Kotlin 后端微服务(结构化并发)
✅ 数据库查询并发(async + await 并行请求)
✅ 需要简洁代码风格(同步写法异步执行)
❌ 纯 CPU 计算(用协程没优势,直接用线程池)
❌ 简单同步程序(引入协程增加复杂度)


全方位对比表

维度

Linux 进程

JVM 进程

JVM 线程

Node.js Worker

HarmonyOS Actor

Kotlin 协程

运行层级

OS 内核

OS 内核 + JVM

JVM + OS

V8 Isolate + OS

ArkRuntime + OS

用户态库

创建耗时

60–20,000 μs

同上 + ~100ms 启动

~10 μs

~5 ms

~μs

纳秒级

内存占用/实例

100 MB+

200 MB+

~1 MB

~5–10 MB

~1–2 MB

~100 字节

最大并发数

数百

数十~数百

数千

数百

64 (Worker)

100,000+

调度方式

内核抢占式

内核抢占式

内核抢占式

事件循环 + 线程池

线程池 + 抢占

用户态协作式

内存模型

物理隔离 (MMU)

物理隔离 + JMM

进程内共享堆

Isolate (独立 V8)

Actor 逻辑隔离

共享线程堆

通信方式

IPC (pipe/shm)

IPC

共享变量

MessageChannel / SAB

消息序列化 / Sendable

直接调用(同线程)

隔离强度

★★★★★ 硬件

★★★★★ 硬件

★☆☆☆☆ 无隔离

★★☆☆☆ Isolate

★★★★☆ 运行时

☆☆☆☆☆ 无隔离

数据同步

IPC 序列化

IPC 序列化

锁 (sync/Lock)

Atomics

消息 / AsyncLock

无需(串行)

竞态条件

⚠️ 存在

⚠️ SAB 时存在

无(默认)/ ⚠️ Sendable 时

无(同线程串行)

死锁风险

无(不共享)

⚠️ 高

⚠️ SAB 时

无(默认)/ ⚠️ 嵌套锁

无(无锁)

故障影响

隔离(独立)

隔离(独立)

影响同进程

可影响同进程

隔离

影响同线程

挂起代价

高(进程上下文)

低(线程上下文)

极低(不阻塞线程)

最好在哪

安全第一

企业级后端

通用并行计算

高并发 I/O

安全+隔离

高并发 I/O


层次包含关系:一张图看清所有层级

┌──────────────────────────────────────────────────────────────────────┐
│                        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 个并发任务频繁读写

方案

怎么实现

痛点

Linux 进程

进程 A 持有计数器,其他进程通过 IPC 读写

IPC 毫秒级延迟 → 高频计数器完全不可用

JVM 线程

AtomicInteger

CAS 自旋在高竞争下退化,但数据是实时的

Node.js Event Loop

单线程内存变量

最优解! 单线程没有竞态,无需锁

Node.js Worker

SharedArrayBuffer + Atomics

比 JVM 的 Atomic 更底层,心智负担高

HarmonyOS Actor

postMessage 通知一个 Actor 更新

序列化开销 > 计数器操作本身

Kotlin 协程

单线程调度Mutex

协程的 I/O 优势在这里体现不出来

这个场景的赢家是 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. 每个模型都有"设计时针对的场景"和"设计时放弃的东西"

模型

为谁设计

放弃了什么

Linux 进程

安全可靠

创建速度和通信效率

JVM 线程

通用并行

并发安全性(让开发者自己保证)

Kotlin 协程

高并发 I/O

CPU 密集效率、调试可读性

Node.js Event Loop

高并发网络 I/O

CPU 密集型(单线程阻塞)

Node.js Worker Threads

CPU 并行

简单共享内存(需 SAB + Atomics)

HarmonyOS Actor

安全隔离 + 并发

共享内存的便利和效率

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 的代价到底在哪。


文章作者: 嘿手大叔
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 i·Space
HarmonyOS
喜欢就支持一下吧
打赏
微信 微信
支付宝 支付宝