在计算机领域中,我们通常要进行多任务处理。计算机操作系统通过多核/多线程调度(CPU流水线)以及上下文切换为多任务处理提供了很好的支持。

但是如何充分利用现有系统资源,最大限度地提高应用系统的业务请求处理能力一直是个热门话题。我们将并发系统的设计划分为整体框架与要素同步与通信机制以及并发调度模型实现三个关键层次。

并发模型框架与核心要素

一个完整的并发模型/并发系统框架,通常由以下几个核心组件与维度构成:

  1. 执行单元与调度 (Scheduling & Execution Units)

    • 执行主体:内核态线程 (Kernel Thread)、用户态轻量级线程/协程 (Goroutine / Virtual Thread)、事件循环 (Event Loop Handler)。
    • 调度策略:抢占式调度 (Preemptive Scheduling)、协作式调度 (Cooperative Scheduling)、工作偷取 (Work Stealing)。
    • 运行上下文:上下文切换开销(寄存器/栈保存)、内存占用与动态扩缩栈。
  2. 同步与通信机制 (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)。
  3. 异常处理与容错机制 (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)

这是最传统的并发通信方式,多个线程共享同一块内存地址空间。

CSP 模型与 Channel (Go)

CSP(Communicating Sequential Processes,通信顺序进程)是计算机科学家 C.A.R. Hoare 于 1978 年提出的一种并发模型哲学。

如果用一句话概括 CSP 的核心思想,那就是:将并发程序拆分为多个独立运行的顺序进程(Sequential Processes),进程之间不共享状态,仅通过显式的消息通道(Channels)进行通信与同步。

CSP 的核心三要素

CSP 模型的构建基于三个非常直观的基础概念:

图解:传统共享内存 vs CSP 模型

[ Thread A ] --( Write )--> [ 共享内存区 (Shared Memory) ] <-- ( Read )-- [ Thread B ]
                                 [ 锁 (Mutex) 保护 ]
[ Process A ] --( Send Data )--> [ Channel (管道) ] --( Recv Data )--> [ Process B ]
D2 Diagram
qtopie.github.io

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 的概念直接做成了语言级关键字和内置数据结构

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 内部字段安全的互斥锁
}
D2 Diagram
qtopie.github.io
Channel 的核心收发逻辑与 G 挂起唤醒流程

重点:当 Goroutine 在 Channel 上阻塞时,它仅仅是在用户态被 Go 运行时挂起(gopark),底层操作系统线程 M 并不会被阻塞,M 会立刻切换去运行其他可运行的 G。

核心总结与思维转变

理解 CSP 模型,最本质的是理解一种解耦与思维方式的转变


无锁环形队列 (Ring Buffer)

如果追求的是极低延迟与超高吞吐的单机并发通信(例如日志系统、网络数据包缓冲区、音视频流处理),无锁环形队列(Ring Buffer)是最常用的高性能方案。

核心原理

环形队列本质上是一个固定大小的数组,通过头指针(Head,写位置)和尾指针(Tail,读位置)在数组上循环移动,实现高效的数据交接。

flowchart LR P1[Producer 1] --> RB[Ring Buffer] P2[Producer 2] --> RB RB --> C1[Consumer 1] RB --> C2[Consumer 2]
D2 Diagram
qtopie.github.io

为什么高效?

  1. 无锁设计:在单生产者单消费者(SPSC)场景下,写指针仅由生产者更新,读指针仅由消费者更新,两端互不干涉,实现完全无锁
  2. 预分配内存:数组在初始化时一次性分配好,运行期间原位覆盖,避免了频繁创建/销毁对象带来的 GC 压力。
  3. 快速取模寻址:当数组长度设为 $2^n$ 时,通过位运算 index & (capacity - 1) 快速计算数组下标,比普通取模(%)高效得多。

典型应用场景

Java 经典实现:ArrayDeque

Java 标准库中的 java.util.ArrayDequeDeque 双端队列)内部就是一个典型的环形缓冲区(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;

并发调度模型实现

在理解了整体框架与通信机制后,本部分重点剖析常见的并发调度模型及其工程实现。

模型核心机制适合场景复杂度
多线程/线程池OS 调度计算密集型、小规模并发
协程 (Coroutines)用户态调度高并发 Web 服务、I/O 密集型低 (开发体验好)
事件驱动 (NIO)状态机/回调网关、长连接、代理服务
Actor 模型消息传递 (Mailbox)分布式、高可靠、无锁化设计

基于多线程的任务并行 (Thread-based / Preemptive Multitasking)

这是最经典的线程池所属的范畴。

flowchart LR Client[Client Requests] --> ES[ExecutorService] ES --> Queue[Blocking Queue] ES --> WT[Worker Threads] Queue --> WT WT --> Task[Run Task] WT --> Queue OS[OS Scheduler] -. time slice .- WT

经典实现:Java ThreadPool (ThreadPoolExecutor)

原理:线程池通过预先创建一定数量的线程,复用这些线程来执行大量短生命周期的任务,避免频繁创建/销毁线程的开销。

核心架构 D2 图示:

D2 Diagram
qtopie.github.io
核心路径:任务与线程数的动态伸缩(Growth & Shrinking)

ThreadPoolExecutor 的核心设计在于基于任务负载动态调控工作线程数量。任务提交与线程数变化的决策链如下:

  1. 动态增长路径(Task Submission & Growth)

当调用 execute(task) 提交任务时,处理逻辑与线程扩展顺序如下:

D2 Diagram
qtopie.github.io
  1. 动态收缩与回收路径(Idle Timeout & Shrinking)

当突发流量过去后,多余的线程如何优雅回收:

D2 Diagram
qtopie.github.io
线程生命周期与状态流转

ThreadPoolExecutor 在内部通过一个原子整数 ctl(包含线程池状态 runState 和有效工作线程数 workerCount)统一管控线程池本身和内部工作线程(Worker)的生命周期。

D2 Diagram
qtopie.github.io
工作线程 (Worker) 的源码生命周期全景图
D2 Diagram
qtopie.github.io
工作线程 (Worker) 的具体源码实现关要

Java 线程池并不直接管理原始 Thread 对象,而是将其包装为 Worker 内部类(继承自 AQS 并实现 Runnable)。

  1. addWorker(Runnable firstTask, boolean core):加锁并递增 CAS workerCount,创建 Worker 实例并启动线程。
  2. runWorker(Worker w):Worker 的 run() 方法调用的核心逻辑 loop。首先处理 firstTask,随后死循环调用 getTask()
  3. getTask():根据 workerCountcorePoolSize 选择 workQueue.poll(keepAliveTime) 还是 workQueue.take()。返回 null 是 Worker 退出的唯一信号。
  4. 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 虚拟线程就是典型的实现。

flowchart LR App[App] --> G[Goroutines] G --> Sch[Go Scheduler] Sch --> P[P: Processor] P --> M[M: OS Thread] M --> OS[Kernel]

经典实现:Go Routine (GMP 模型)

原理:Goroutine 是 Go 运行时 (Runtime) 调度的用户态轻量级线程,创建和切换开销极小(栈空间从 2KB 动态伸缩)。Go 运行时采用 GMP 调度模型 来管理多协程在多核 OS 线程上的高效并发运行。

GMP 核心组件
GMP 调度架构图
D2 Diagram
qtopie.github.io
P 与 M 的动态绑定关系

在任意时刻,正在运行 Go 代码的 M 数量和 P 的数量是 1:1 的,但从整体系统中的 P 和 M 的总量来看,它们并不是 1:1 一一对应的

为什么 M 的数量会大于 P?(Hand-off 机制导致的 M-P 动态解绑)
D2 Diagram
qtopie.github.io
  1. 当发生阻塞系统调用 (Syscall) 时
    • 假设 M1 绑定的 G1 执行了一个阻塞的 Cgo 或文件/系统调用,M1 线程会被操作系统内核阻塞。
    • 此时 Go 监控线程(sysmon)会把 P1 从 M1 上剥离(Hand-off)
    • P1 会被移交给一个新创建(或处于休眠线程池中)的 M2 绑定,使得 P1 本地队列里的其他 Goroutine 可以继续运行。
    • 此时系统中产生了 2 个 M(M1 阻塞,M2 运行)对应 1 个 P1
  2. 当阻塞系统调用返回后
    • M1 试图寻找空闲的 P 绑定以继续运行 G1。
    • 如果找到了空闲的 P,则重新绑定;
    • 如果没有空闲的 P,M1 会将 G1 放回全局队列 (GRQ),而 M1 自身则进入休眠线程池(Midle),等待下一次被唤醒。
GMP 核心调度策略:Work Stealing 与 Hand-off
  1. Work Stealing (工作偷取)
    • 当某个 M 绑定的 P1 本地队列 lrq1 已经空了时,它不会直接阻塞 M,而是会:
      1. 尝试从全局队列 GRQ 批量获取任务;
      2. 若全局队列也为空,随机挑选另一个 P2,从 lrq2 偷取一半的 G 过来运行,极大地提高了 CPU 多核利用率并减少加锁竞争。
  2. Hand-off (抢占/分离解绑)
    • 当 G1 发起系统调用(Syscall)导致 M1 陷入阻塞时:
    • Go 调度器(Sysmon)会将 P1 与 M1 解绑,并寻找/创建另一个空闲的 M3 与 P1 绑定,让 P1 继续执行本地队列中的其他 G。
    • 当 M1 从系统调用返回后,会尝试获取空闲 P,若失败则将 G1 放回全局队列并将 M1 放入休眠线程池。
深度解析:传统 Java 线程池争抢全局队列锁的性能瓶颈

在传统 Java 线程池(如 ThreadPoolExecutor 使用 LinkedBlockingQueueArrayBlockingQueue)中,架构设计核心是单对多模型(一个共享任务队列对多个 Worker 线程):

D2 Diagram
qtopie.github.io

瓶颈产生的深层逻辑与硬件影响:

  1. 粗粒度互斥锁争抢 (Lock Contention):所有 Worker 线程在执行完手头任务后,都要同时去同一个 BlockingQueue 调 poll()take() 抢任务。
  2. CPU 缓存失效 (Cache Line Bouncing / False Sharing):多个 Worker 线程频繁修改阻塞队列的头尾指针,导致 Cache Line 在多个 CPU 核心之间频繁失效。
  3. 高 QPS/短任务场景下的性能雪崩:争抢锁与上下文切换的开销远大于任务本身。

Go GMP 如何彻底解除锁瓶颈?

D2 Diagram
qtopie.github.io
  1. 独立本地队列 (Local Run Queue, LRQ):绑定的 M 消费当前 P 的本地队列时完全无需与其他 M/P 争抢全局锁。
  2. Work Stealing (工作偷取算法):极大地降低了锁冲突概率。
  3. 全局队列 (Global Run Queue) 仅作为兜底
GMP 模型 vs Java ThreadPool 对比
维度Java ThreadPoolGo 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 是轻量级任务。

优势:

典型应用:Web 服务、高并发网关、分布式系统

简单代码示例:

go func() {
	fmt.Println("Hello Goroutine")
}()

事件驱动与非阻塞 I/O (Event-driven / NIO)

这是 Node.jsNetty 能够支撑高并发的秘密。

Netty:

flowchart LR Client[Clients] --> Channel[Channel] Channel --> EventLoop[EventLoop Group] EventLoop --> Selector[NIO Selector/epoll] EventLoop --> Pipeline[ChannelPipeline] Pipeline --> H1[Handler In] H1 --> H2[Handler Out] H2 --> Channel

Redis:

flowchart LR C1[Client 1] --> EV[Redis Event Loop] C2[Client 2] --> EV C3[Client N] --> EV EV --> EP[epoll/IO Multiplexing] EV --> Q[Command Queue] Q --> Exec[Single-thread Execute] Exec --> EV

经典实现:Netty

原理:基于 Java NIO,采用 Reactor 事件驱动模型,单线程可处理成千上万连接。

核心架构 D2 图示:

D2 Diagram
qtopie.github.io

优势:

典型应用:分布式系统通信、微服务网关

简单代码示例:

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 的后端)。

flowchart LR A1[Actor A] --> M1[Mailbox A] A2[Actor B] --> M2[Mailbox B] A3[Actor C] --> M3[Mailbox C] M1 --> A1 M2 --> A2 M3 --> A3 A1 -- message --> M2 A2 -- message --> M3 A3 -- message --> M1

经典实现:Akka

原理:基于 Actor 模型,每个 Actor 独立、无共享状态,通过消息传递通信。天然无锁,易于扩展。

核心架构 D2 图示:

D2 Diagram
qtopie.github.io

优势:

典型应用: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"