在计算机领域中,我们通常要进行多任务处理。计算机操作系统通过多核/多线程调度(CPU流水线)以及上下文切换为多任务处理提供了很好的支持。
但是如何充分利用现有系统资源,最大限度地提高应用系统的业务请求处理能力一直是个热门话题。我们将并发系统的设计划分为整体框架与要素、同步与通信机制以及并发调度模型实现三个关键层次。
并发模型框架与核心要素
一个完整的并发模型/并发系统框架,通常由以下几个核心组件与维度构成:
执行单元与调度 (Scheduling & Execution Units)
- 执行主体:内核态线程 (Kernel Thread)、用户态轻量级线程/协程 (Goroutine / Virtual Thread)、事件循环 (Event Loop Handler)。
- 调度策略:抢占式调度 (Preemptive Scheduling)、协作式调度 (Cooperative Scheduling)、工作偷取 (Work Stealing)。
- 运行上下文:上下文切换开销(寄存器/栈保存)、内存占用与动态扩缩栈。
同步与通信机制 (Synchronization & Communication)
- 共享内存与锁 (Shared Memory & Locks):互斥锁 (Mutex)、读写锁 (RWMutex)、自旋锁 (Spinlock)、悲观锁/乐观锁。
- 无锁同步 (Lock-free Synchronization):CAS (Compare-And-Swap)、原子指令 (Atomics)、内存屏障 (Memory Barrier)。
- 消息传递与管道 (Message Passing & Channels):CSP 模型 (Channel)、Actor 模型的 Mailbox、无锁环形队列 (Ring Buffer)。
异常处理与容错机制 (Error Handling & Fault Tolerance)
- 异常传播:跨线程/协程的异常捕获与传递 (Futures/Promises/Panic Recovery)。
- 监督者模式 (Supervisor Model):Actor 中的“任其崩溃” (Let it crash) 哲学与树状监督树 (Supervision Tree)。
- 资源泄漏控制:结构化并发 (Structured Concurrency)、上下文取消 (Context Cancellation/Timeout)。
同步与通信机制
并发单元之间需要配合与协作,同步与通信机制奠定了并发系统的效率与稳定性。
共享内存与锁机制 (Shared Memory & Lock Mechanism)
这是最传统的并发通信方式,多个线程共享同一块内存地址空间。
- 互斥锁与临界区:通过操作系统信号量或内核锁保护共享资源,避免竞态条件 (Race Condition),但存在锁竞争与上下文切换开销。
- CAS 与无锁数据结构:利用 CPU 提供的硬件级原子指令 (
CMPXCHG) 实现无锁更新,减少上下文切换,但在高并发下可能产生 ABA 问题和自旋 CPU 浪费。
CSP 模型与 Channel (Go)
CSP(Communicating Sequential Processes,通信顺序进程)是计算机科学家 C.A.R. Hoare 于 1978 年提出的一种并发模型哲学。
如果用一句话概括 CSP 的核心思想,那就是:将并发程序拆分为多个独立运行的顺序进程(Sequential Processes),进程之间不共享状态,仅通过显式的消息通道(Channels)进行通信与同步。
CSP 的核心三要素
CSP 模型的构建基于三个非常直观的基础概念:
进程(Process):
- 指的是独立的、顺序执行的代码块。
- 在 CSP 视角下,每个进程都有自己私有的状态和内存空间,内部逻辑是顺序执行的,极易推导和理解。
- 在 Go 中,Goroutine 就是“进程”的具体实现;在 Erlang 中,对应的是 Actor 进程。
通道(Channel):
- 指的是连接不同进程之间的第一公民(First-class)消息管道。
- 通道是数据流转的唯一桥梁。数据从一端写入,从另一端读出。
通信(Communication):
- 进程间交接数据的行为,本身也是同步点(Synchronization Point)。
- 当发送方向无缓冲通道发送消息时,它会暂停执行,直到接收方准备好读取数据。这种“通过通信实现同步”的机制消除了传统的互斥锁(Mutex)。
图解:传统共享内存 vs CSP 模型
- 传统多线程模式(共享内存 + 加锁):
[ Thread A ] --( Write )--> [ 共享内存区 (Shared Memory) ] <-- ( Read )-- [ Thread B ]
▲
[ 锁 (Mutex) 保护 ]
痛点:线程 A 和线程 B 同时竞争同一块内存。你必须小心翼翼地加锁/解锁。一旦漏锁、锁顺序不一致,就会导致数据竞态(Data Race)或死锁(Deadlock)。
CSP 模式(管道通信):
[ Process A ] --( Send Data )--> [ Channel (管道) ] --( Recv Data )--> [ Process B ]
- 优势:Process A 把数据送入 Channel 后,逻辑上就交出(Transfer)了数据的所有权。Process A 不再去碰那块内存,Process B 拿到后独占处理。无需显式加锁,也天然不存在内存竞争。
CSP 与 Actor 模型的对比
同样是基于“消息传递”的并发模型,CSP 经常会被用来与 Erlang/Akka 中的 Actor 模型 做对比,两者的差异非常微妙:
| 对比维度 | CSP 模型(如 Go) | Actor 模型(如 Erlang / Akka) |
|---|---|---|
| 通信主体 | 以 Channel(通道)为核心 | 以 Actor(实体)为核心 |
| 寻址方式 | 匿名通信:发送方只需把消息丢进 Channel,不需要知道是谁来接收。 | 直接寻址:每个 Actor 有唯一的 Mailbox/PID,发送方必须明确知道“发给谁”。 |
| 耦合度 | 进程与进程解耦,只依赖共同引用的 Channel。 | 发送方与接收方存在标识(PID)绑定。 |
| 同步/异步 | 原生通道支持严格的同步阻塞交接(无缓冲 channel)。 | 消息传递通常是默认异步的(投递到 Mailbox 后立刻返回)。 |
CSP 在 Go 语言中的落地方案与底层实现
Go 语言是现代编程语言中最成功实践 CSP 理论的代表。Go 语言将 CSP 的概念直接做成了语言级关键字和内置数据结构:
go关键字:极轻量地启动一个 CSP 进程(Goroutine),初始栈仅 2KB,由 GMP 调度器在用户态完成调度。chan关键字:内建的第一公民数据结构(对应 CSP Channel),支持同步管道(无缓冲)与异步管道(带缓冲区)。select关键字:允许一个 Goroutine 同时在多个 Channel 上进行多路复用(Multiplexing),任意 Channel 准备就绪即可触发相应分支,这是 CSP 模型中处理多路异步事件的核心能力。
Channel 的底层数据结构 (hchan)
Go 语言在 runtime 内部将 Channel 实现为一个名为 hchan 的结构体(位于 runtime/chan.go):
type hchan struct {
qcount uint // 当前环形队列中剩余的元素个数
dataqsiz uint // 环形缓冲区 (buf) 的总容量 (make(chan T, cap))
buf unsafe.Pointer // 指向环形队列数组的指针 (有缓冲 channel)
elemsize uint16
closed uint32 // 管道关闭标识 (0/1)
sendx uint // 缓冲区发送索引 (Buffer Write Index)
recvx uint // 缓冲区接收索引 (Buffer Read Index)
recvq waitq // 因接收而阻塞挂起的 G 双向等待链表 (sudog 链表)
sendq waitq // 因发送而阻塞挂起的 G 双向等待链表 (sudog 链表)
lock mutex // 保护 hchan 内部字段安全的互斥锁
}
Channel 的核心收发逻辑与 G 挂起唤醒流程
无缓冲 / 缓冲区已满时发送 (
ch <- data):- 若
recvq链表非空(已有在等待接收的 G):直接绕过buf,把数据从 Sender 栈内存拷贝到 Receiver G 的栈内存中,并调用goready()将该 Receiver G 唤醒放入 P 本地队列。 - 若
buf还有空余:加锁,将数据写入buf[sendx],更新sendx索引,解锁。 - 若
buf已满且无recvq:当前 Sender G 创建sudog对象封装自身,放入sendq链表,然后调用gopark()将自己主动挂起(切换为_Gwaiting状态,让出 M 给其他 G 执行)。
- 若
无缓冲 / 缓冲区为空时接收 (
data := <-ch):- 若
sendq链表非空(已有在等待发送的 G):- 若无缓冲:直接从 Sender G 栈内存拷贝数据到当前 Receiver G 栈,唤醒 Sender G。
- 若有缓冲:从
buf[recvx]读取数据,并将sendq头部 G 的数据补写进buf[sendx],唤醒 Sender G。
- 若
buf有数据:从buf[recvx]读数据,更新recvx索引。 - 若
buf为空且无sendq:当前 Receiver G 封装sudog挂入recvq链表,调用gopark()挂起。
- 若
重点:当 Goroutine 在 Channel 上阻塞时,它仅仅是在用户态被 Go 运行时挂起(gopark),底层操作系统线程 M 并不会被阻塞,M 会立刻切换去运行其他可运行的 G。
核心总结与思维转变
理解 CSP 模型,最本质的是理解一种解耦与思维方式的转变:
- 传统思维:把并发看作“多个 CPU 核心抢夺同一个数据存储区”。
- CSP 思维:把并发看作“流水线上的工人的传递零件”。每个人只管在自己的桌子上做好自己的工序,然后通过传送带(Channel)交接给下一个工人。
无锁环形队列 (Ring Buffer)
如果追求的是极低延迟与超高吞吐的单机并发通信(例如日志系统、网络数据包缓冲区、音视频流处理),无锁环形队列(Ring Buffer)是最常用的高性能方案。
核心原理
环形队列本质上是一个固定大小的数组,通过头指针(Head,写位置)和尾指针(Tail,读位置)在数组上循环移动,实现高效的数据交接。
为什么高效?
- 无锁设计:在单生产者单消费者(SPSC)场景下,写指针仅由生产者更新,读指针仅由消费者更新,两端互不干涉,实现完全无锁。
- 预分配内存:数组在初始化时一次性分配好,运行期间原位覆盖,避免了频繁创建/销毁对象带来的 GC 压力。
- 快速取模寻址:当数组长度设为 $2^n$ 时,通过位运算
index & (capacity - 1)快速计算数组下标,比普通取模(%)高效得多。
典型应用场景
- 高性能网络通信:Linux 内核
ring_buffer/ DPDK 报文接收队列。 - 进程内异步队列:高性能日志框架的异步日志队列。
- 音视频/流媒体:音视频帧数据流平滑缓冲区。
Java 经典实现:ArrayDeque
Java 标准库中的 java.util.ArrayDeque(Deque 双端队列)内部就是一个典型的环形缓冲区(Ring Buffer):它用一段长度恒为 $2^n$ 的连续数组模拟环形空间,通过 head(队首)与 tail(队尾)两个指针在数组上循环移动,两端都能以 O(1) 复杂度入队/出队。
// 核心字段(JDK 源码简化)
Object[] elements; // 环形数组,长度恒为 2 的幂
int head; // 队首指针,指向第一个元素
int tail; // 队尾指针,指向下一个可写入位置
环形寻址的关键在于位运算回绕:因为数组长度是 2 的幂,(tail + 1) & (elements.length - 1) 等价于 (tail + 1) % elements.length,但比取模快得多:
// 队尾入队 addLast(E e)
elements[tail] = e;
tail = (tail + 1) & (elements.length - 1); // 回绕到数组起点
if (tail == head) doubleCapacity(); // 队列满则翻倍扩容
// 队首出队 pollFirst()
E e = (E) elements[head];
elements[head] = null; // 置空帮助 GC
head = (head + 1) & (elements.length - 1);
return e;
- 容量恒为 2 的幂:
doubleCapacity()将数组长度翻倍并重排元素,保证始终能用位运算完成下标回绕。 head == tail判满/判空:队列为空时两指针重合,写入后tail追上head表示已满(需扩容),由此区分环形空间的状态。- 非线程安全:
ArrayDeque自身不加锁,多线程并发使用需外部同步;高并发无锁场景可选用 LMAX Disruptor 或ConcurrentLinkedDeque等实现。
并发调度模型实现
在理解了整体框架与通信机制后,本部分重点剖析常见的并发调度模型及其工程实现。
| 模型 | 核心机制 | 适合场景 | 复杂度 |
|---|---|---|---|
| 多线程/线程池 | OS 调度 | 计算密集型、小规模并发 | 中 |
| 协程 (Coroutines) | 用户态调度 | 高并发 Web 服务、I/O 密集型 | 低 (开发体验好) |
| 事件驱动 (NIO) | 状态机/回调 | 网关、长连接、代理服务 | 高 |
| Actor 模型 | 消息传递 (Mailbox) | 分布式、高可靠、无锁化设计 | 中 |
基于多线程的任务并行 (Thread-based / Preemptive Multitasking)
这是最经典的线程池所属的范畴。
- 核心逻辑: 操作系统内核负责线程切换。每个线程有独立的栈空间。
- 优点: 能够充分利用多核 CPU。
- 缺点: 线程是很“重”的资源(通常 1MB 左右),上下文切换(Context Switch)开销大。当并发量达到万级时,内存会爆掉,CPU 也会忙于切换而不是干活。
经典实现:Java ThreadPool (ThreadPoolExecutor)
原理:线程池通过预先创建一定数量的线程,复用这些线程来执行大量短生命周期的任务,避免频繁创建/销毁线程的开销。
核心架构 D2 图示:
核心路径:任务与线程数的动态伸缩(Growth & Shrinking)
ThreadPoolExecutor 的核心设计在于基于任务负载动态调控工作线程数量。任务提交与线程数变化的决策链如下:
- 动态增长路径(Task Submission & Growth)
当调用 execute(task) 提交任务时,处理逻辑与线程扩展顺序如下:
- 阶梯式增长策略:
- Phase 1 (创建 Core 线程):只要
workerCount < corePoolSize,即使当前有空闲的核心线程,依然会为新任务优先创建新线程(快速达到corePoolSize稳定吞吐)。 - Phase 2 (任务入队缓冲):达到
corePoolSize后,新任务优先放入阻塞队列workQueue堆积缓冲。 - Phase 3 (突发流量扩展 Max 线程):若队列填满(
offer()返回false),且workerCount < maximumPoolSize,则打破 core 限制,创建非核心线程(救急线程)直接处理该任务。 - Phase 4 (饱和拒绝):若线程已达到
maximumPoolSize且队列已满,则触发拒绝策略。
- Phase 1 (创建 Core 线程):只要
- 动态收缩与回收路径(Idle Timeout & Shrinking)
当突发流量过去后,多余的线程如何优雅回收:
- 收缩机制与心跳回收:
- 每个工作线程在处理完手头任务后,都会在
runWorker()的while循环中调用getTask()。 - 在
getTask()内计算判断:若workerCount > corePoolSize(或开启了allowCoreThreadTimeOut),线程会从阻塞队列拉取任务的模式由阻塞take()切换为超时拉取poll(keepAliveTime, TimeUnit)。 - 一旦在
keepAliveTime内没有收到新任务,poll()会返回null,导致getTask()返回null。 runWorker()收到null后退出循环,进入processWorkerExit():加锁将当前Worker从workers集合移除,更新 CAS 计数,Java 线程自然执行完毕并被 JVM/OS 回收。
- 每个工作线程在处理完手头任务后,都会在
线程生命周期与状态流转
ThreadPoolExecutor 在内部通过一个原子整数 ctl(包含线程池状态 runState 和有效工作线程数 workerCount)统一管控线程池本身和内部工作线程(Worker)的生命周期。
工作线程 (Worker) 的源码生命周期全景图
工作线程 (Worker) 的具体源码实现关要
Java 线程池并不直接管理原始 Thread 对象,而是将其包装为 Worker 内部类(继承自 AQS 并实现 Runnable)。
addWorker(Runnable firstTask, boolean core):加锁并递增 CASworkerCount,创建 Worker 实例并启动线程。runWorker(Worker w):Worker 的run()方法调用的核心逻辑 loop。首先处理firstTask,随后死循环调用getTask()。getTask():根据workerCount与corePoolSize选择workQueue.poll(keepAliveTime)还是workQueue.take()。返回null是 Worker 退出的唯一信号。processWorkerExit(Worker w, boolean completedAbruptly):Worker 退出的清理阶段,负责清理引用、更新ctl以及尝试向状态TIDYING推进。
优势:
- 线程复用,减少资源消耗
- 支持任务队列、拒绝策略、灵活参数配置
- 适合服务器端、爬虫、批量处理等场景
典型应用:Web 服务器、RPC 框架、异步任务调度
简单代码示例:
ExecutorService pool = new ThreadPoolExecutor(
4, // corePoolSize
8, // maximumPoolSize
60, TimeUnit.SECONDS, // keepAliveTime
new LinkedBlockingQueue<>(100) // task queue
// RejectedExecutionHandler (default AbortPolicy)
);
pool.submit(() -> System.out.println("Hello ThreadPool"));
pool.shutdown();
协作式多任务:协程 (Coroutines / User-level Threads)
这是目前高并发的主流,比如 Goroutine / JVM 虚拟线程就是典型的实现。
- 核心逻辑: 在用户态调度,而非内核态。协程在遇到 I/O 阻塞时自动让出执行权,而不需要销毁线程。
- 代表: Go (Goroutines), Kotlin (Coroutines), Python (asyncio), Java (Project Loom/Virtual Threads)。
- 特点: 极轻量,单机支持百万级协程。
经典实现:Go Routine (GMP 模型)
原理:Goroutine 是 Go 运行时 (Runtime) 调度的用户态轻量级线程,创建和切换开销极小(栈空间从 2KB 动态伸缩)。Go 运行时采用 GMP 调度模型 来管理多协程在多核 OS 线程上的高效并发运行。
GMP 核心组件
- G (Goroutine):协程,包含栈内存、指令指针(PC)及调度上下文信息。
- M (Machine):操作系统物理线程,由 OS 内核调度,负责真正执行代码。
- P (Processor):逻辑处理器/调度上下文(通常数量等于 CPU 逻辑核数
GOMAXPROCS)。P 持有本地协程队列(LRQ),M 必须绑定 P 才能运行 G。
GMP 调度架构图
P 与 M 的动态绑定关系
在任意时刻,正在运行 Go 代码的 M 数量和 P 的数量是 1:1 的,但从整体系统中的 P 和 M 的总量来看,它们并不是 1:1 一一对应的。
- P 的数量 (固定):由环境/配置决定,默认等于 CPU 逻辑核数(
GOMAXPROCS,如 8 核机器则P=8)。P 代表并发执行 Go 代码的凭证/许可证,系统中同时最多只有P个 M 能处于运行状态。 - M 的数量 (动态):Go 运行时允许创建的 M 的默认上限是 10000 个。M 代表实际的 OS 内核线程,其数量通常大于 P 的数量。
为什么 M 的数量会大于 P?(Hand-off 机制导致的 M-P 动态解绑)
- 当发生阻塞系统调用 (Syscall) 时:
- 假设 M1 绑定的 G1 执行了一个阻塞的 Cgo 或文件/系统调用,M1 线程会被操作系统内核阻塞。
- 此时 Go 监控线程(
sysmon)会把 P1 从 M1 上剥离(Hand-off)。 - P1 会被移交给一个新创建(或处于休眠线程池中)的 M2 绑定,使得 P1 本地队列里的其他 Goroutine 可以继续运行。
- 此时系统中产生了 2 个 M(M1 阻塞,M2 运行)对应 1 个 P1。
- 当阻塞系统调用返回后:
- M1 试图寻找空闲的 P 绑定以继续运行 G1。
- 如果找到了空闲的 P,则重新绑定;
- 如果没有空闲的 P,M1 会将 G1 放回全局队列 (GRQ),而 M1 自身则进入休眠线程池(Midle),等待下一次被唤醒。
GMP 核心调度策略:Work Stealing 与 Hand-off
- Work Stealing (工作偷取):
- 当某个 M 绑定的 P1 本地队列
lrq1已经空了时,它不会直接阻塞 M,而是会:- 尝试从全局队列 GRQ 批量获取任务;
- 若全局队列也为空,随机挑选另一个 P2,从
lrq2偷取一半的 G 过来运行,极大地提高了 CPU 多核利用率并减少加锁竞争。
- 当某个 M 绑定的 P1 本地队列
- Hand-off (抢占/分离解绑):
- 当 G1 发起系统调用(Syscall)导致 M1 陷入阻塞时:
- Go 调度器(Sysmon)会将 P1 与 M1 解绑,并寻找/创建另一个空闲的 M3 与 P1 绑定,让 P1 继续执行本地队列中的其他 G。
- 当 M1 从系统调用返回后,会尝试获取空闲 P,若失败则将 G1 放回全局队列并将 M1 放入休眠线程池。
深度解析:传统 Java 线程池争抢全局队列锁的性能瓶颈
在传统 Java 线程池(如 ThreadPoolExecutor 使用 LinkedBlockingQueue 或 ArrayBlockingQueue)中,架构设计核心是单对多模型(一个共享任务队列对多个 Worker 线程):
瓶颈产生的深层逻辑与硬件影响:
- 粗粒度互斥锁争抢 (Lock Contention):所有 Worker 线程在执行完手头任务后,都要同时去同一个 BlockingQueue 调
poll()或take()抢任务。 - CPU 缓存失效 (Cache Line Bouncing / False Sharing):多个 Worker 线程频繁修改阻塞队列的头尾指针,导致 Cache Line 在多个 CPU 核心之间频繁失效。
- 高 QPS/短任务场景下的性能雪崩:争抢锁与上下文切换的开销远大于任务本身。
Go GMP 如何彻底解除锁瓶颈?
- 独立本地队列 (Local Run Queue, LRQ):绑定的 M 消费当前 P 的本地队列时完全无需与其他 M/P 争抢全局锁。
- Work Stealing (工作偷取算法):极大地降低了锁冲突概率。
- 全局队列 (Global Run Queue) 仅作为兜底。
GMP 模型 vs Java ThreadPool 对比
| 维度 | Java ThreadPool | Go GMP 模型 |
|---|---|---|
| 调度层级 | 线程级(直接管理 OS Thread / Worker) | 协程级(M 对应 Worker Thread,P 对应调度器,G 对应 Task) |
| 任务队列 | 共享阻塞队列(如 LinkedBlockingQueue),多线程存在锁竞争 | 两级队列:独立 P 本地队列 (无锁 CAS) + 全局队列 (有锁) |
| 负载均衡 | 依靠工作线程争抢单一队列 | Work Stealing:空闲 M 绑定的 P 会从其他 P 抢占一半 G |
| 阻塞处理 (I/O/Syscall) | 线程直接阻塞在 I/O/Syscall 上,导致 OS 线程被挂起 | Netpoller (epoll) 解耦 I/O 阻塞;Syscall 触发 Hand-off 解绑 M 和 P |
| 开发体验 | 需显式配置 core/max/queue,容易因阻塞导致线程爆满 | 语言级原生支持 go 关键字,调度对开发者完全透明 |
总结:Go 的 GMP 模型本质上是一个运行在用户态的“两级线程池 + 协程调度器”。M 就是操作系统线程(Worker),P 是调度资源桶,G 是轻量级任务。
优势:
- 单机可支持百万级 goroutine
- 动态两层队列 + Work Stealing 极高吞吐
- 结合 netpoller 无缝处理非阻塞 I/O
典型应用:Web 服务、高并发网关、分布式系统
简单代码示例:
go func() {
fmt.Println("Hello Goroutine")
}()
事件驱动与非阻塞 I/O (Event-driven / NIO)
这是 Node.js 和 Netty 能够支撑高并发的秘密。
- 核心逻辑: 使用 I/O 多路复用技术(如 Linux 的
epoll)。程序不再等待 I/O 完成,而是注册一个回调函数(Callback)。当数据准备好时,操作系统通知程序去处理。 - 优点: 单线程也能处理成千上万个连接,资源消耗极低。
- 缺点: 编程模型复杂(容易陷入回调地狱),不适合 CPU 密集型任务。
Netty:
Redis:
经典实现:Netty
原理:基于 Java NIO,采用 Reactor 事件驱动模型,单线程可处理成千上万连接。
核心架构 D2 图示:
优势:
- 高性能、低延迟
- 支持多种协议和自定义编解码
- 广泛用于 RPC、网关、IM 等
典型应用:分布式系统通信、微服务网关
简单代码示例:
EventLoopGroup boss = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(boss, new NioEventLoopGroup())
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<>() {
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new MyHandler());
}
});
b.bind(8080);
经典实现:Redis 单线程事件循环
原理:单线程事件循环,I/O 多路复用,极致优化命令处理路径。
优势:
- 代码简单,易于维护
- 单线程避免锁竞争
- 支持高并发连接
典型应用:缓存、消息队列、排行榜等
Message Passing
以actor模型来讨论。
这是一种完全不同的思维方式,常见于分布式系统或高可靠应用(如 WhatsApp 的后端)。
- 核心逻辑: 一切皆 Actor。Actor 之间不共享内存,唯一通信方式是发邮件(消息)。
- 优点: 天然无锁。因为不共享变量,所以不存在死锁或竞态条件,扩展性极强。
- 代表: Erlang (OTP), Akka (Scala/Java)。
经典实现:Akka
原理:基于 Actor 模型,每个 Actor 独立、无共享状态,通过消息传递通信。天然无锁,易于扩展。
核心架构 D2 图示:
优势:
- 易于构建分布式、高可靠系统
- 支持容错、监督、远程通信
- 适合事件驱动、解耦场景
典型应用:IM、分布式任务调度、金融风控
简单代码示例(Scala):
class HelloActor extends Actor {
def receive = {
case msg: String => println(s"Hello $msg")
}
}
val system = ActorSystem("sys")
val actor = system.actorOf(Props[HelloActor])
actor ! "Akka"