编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 序卷方法论

  • 数据的本质

  • 运行时模型

  • 并发的设计

    • README
    • 1.线程前世今生探索
    • 2.并发上下文切换原理
    • 3.线程通信设计思想
    • 4.线程异常设计原理
    • 5.多线程并发经典案例
    • 6.并发Bug源头由来
    • 7.并发编程设计思想
    • 8.并发编程安全设计
    • 9.锁核心设计和思想
    • 10.理解CAS设计由来
    • 11.异步和同步的设计
    • 12.单线程模型的思想
      • 01.为什么 C/C++/Java 也在悄悄拥抱单线程
        • 1.1 章首三问(带着问题读完整篇)
        • 1.2 通用本质:单线程不是"省着用 CPU",是"省着用同步"
        • 1.3 通用价值:单线程换来什么、付出什么
        • 1.4 跨语言"单线程"的对应物(不仅 JS!)
        • 1.5 通用方法论:什么时候该选单线程?
        • 1.6 七字真言(贯穿全篇)
        • 1.7 本篇通用收益(与你用什么语言无关)
      • 02.概述与背景
        • 2.1 单线程模型理念
        • 2.2 多线程模型挑战
        • 2.3 要解决的问题
      • 03.认识单线程模型
        • 3.1 餐厅厨房比喻
        • 3.2 单线程模型
        • 3.3 单线程核心思想
        • 3.4 单线程挑战
      • 04.核心设计思想
        • 4.1 协作式VS抢占式
        • 4.2 协作式调度思想
      • 05.协程实现多任务
        • 5.1 并发VS并行
        • 5.2 任务调度器设计
        • 5.3 多任务执行时序图
        • 5.4 协程栈管理
      • 06.多请求设计原理
        • 6.1 请求处理架构
        • 6.2 请求顺序机制
        • 6.3 优先级队列设计
        • 6.4 并发请求处理策略
      • 07.事件循环机制设计
        • 7.1 事件循环核心架构
        • 7.2 事件循环执行流程
        • 7.3 非阻塞I/O设计
      • 08.实际应用分析
        • 8.1 协程VS线程
        • 8.2 实际应用场景
        • 8.3 单线程模型的最佳实践
      • 09.跨语言模型对比
        • 9.1 五大单线程架构上台
        • 9.2 跨语言实现抹出
        • 9.3 Redis单线程能跑100万QPS?
        • 9.4 Nginx Worker隐藏双层设计
      • 10.经典陷阱与反模式
        • 10.1 陷阱①|CPU密集任务阻事件循环
        • 10.2 陷阱②|微任务饱饱
        • 10.3 陷阱③|隐式跨事件循环依赖
        • 10.4 陷阱④|内存泄漏于闭包
        • 10.5 陷阱⑤|错误一步令服务崩溃
      • 11.跨语言单线程陷阱速查表
      • 12.七字真言 × 多语言映射表
      • 13.一句话总结
      • 📎 延伸阅读
    • 13.协程核心设计思想
    • 14.Actor与CSP并发模型
    • 15.线程池的设计思想
    • 16.线程池设计核心原理
    • 17.线程池使用技巧
    • 18.结构化并发设计思想
  • 内存的真相

  • 交互和系统

  • 内功
  • 并发的设计
杨充
2025-11-14
目录

12.单线程模型的思想

# 22.单线程模型的思想

📍 本篇位置:第 3 卷 · 并发之道 · 第 12 篇(范式分流点) 🎯 核心矛盾:多核的诱惑 vs 并发 Bug 的地狱 —— 干脆放弃并发,只用一个线程 + 异步 IO,反而爆红 20 年 🧭 设计灵魂:单线程不是"慢",而是"简单 + 可预测"——代价换来的是零竞态、零锁、零数据竞争 🌐 跨语言覆盖:JavaScript(V8 单线程 + Event Loop) · Node.js(libuv 事件循环) · Redis(单线程命令处理) · Nginx(Worker 进程内单线程) · Dart(Isolate 内单线程) 🔗 延伸阅读:← 21.异步和同步的设计 (opens new window) · → 23.协程核心设计思想 (opens new window) · → 41.消息机制设计思想


# 目录介绍

  • 01.为什么C/C++/Java也在悄悄拥抱单线程
    • 1.1 章首三问(带着问题读完整篇)
    • 1.2 通用本质:单线程不是"省着用 CPU",是"省着用同步"
    • 1.3 通用价值:单线程换来什么、付出什么
    • 1.4 跨语言"单线程"的对应物(不仅 JS!)
    • 1.5 通用方法论:什么时候该选单线程?
    • 1.6 七字真言(贯穿全篇)
    • 1.7 本篇通用收益(与你用什么语言无关)
  • 02.概述与背景
    • 2.1 单线程模型理念
    • 2.2 多线程模型挑战
    • 2.3 要解决的问题
  • 03.认识单线程模型
    • 3.1 餐厅厨房比喻
    • 3.2 单线程模型
    • 3.3 单线程核心思想
    • 3.4 单线程挑战
  • 04.核心设计思想
    • 4.1 协作式VS抢占式
    • 4.2 协作式调度思想
  • 05.协程实现多任务
    • 5.1 并发VS并行
    • 5.2 任务调度器设计
    • 5.3 多任务执行时序图
    • 5.4 协程栈管理
  • 06.多请求设计原理
    • 6.1 请求处理架构
    • 6.2 请求顺序机制
    • 6.3 优先级队列设计
    • 6.4 并发请求处理策略
  • 07.事件循环机制设计
    • 7.1 事件循环核心架构
    • 7.2 事件循环执行流程
    • 7.3 非阻塞I/O设计
  • 08.实际应用分析
    • 8.1 协程VS线程
    • 8.2 实际应用场景
    • 8.3 单线程模型的最佳实践
  • 09.跨语言模型对比
    • 9.1 五大单线程架构上台
    • 9.2 跨语言实现抹出
    • 9.3 Redis单线程能跑100万QPS?
    • 9.4 Nginx Worker隐藏双层设计
  • 10.经典陷阱与反模式
    • 10.1 陷阱①|CPU密集任务阻事件循环
    • 10.2 陷阱②|微任务饱饱
    • 10.3 陷阱③|隐式跨事件循环依赖
    • 10.4 陷阱④|内存泄漏于闭包
    • 10.5 陷阱⑤|错误一步令服务崩溃
  • 11.跨语言单线程陷阱速查表
  • 12.七字真言 × 多语言映射表
  • 13.一句话总结

# 01.为什么 C/C++/Java 也在悄悄拥抱单线程

🧭 本节定位:把"单线程模型"从 JS/Node/Redis 的特定语境上抬到语言无关的通用必要性。读完三问,你将明白单线程不是"性能怪兽们偶然的选择",而是所有重 IO 系统在多核时代的反直觉收敛。

# 1.1 章首三问(带着问题读完整篇)

  1. 为什么 Redis(C)、Nginx(C)、Node(C++) 这些"性能怪兽"反而都是单线程?多核 CPU 不就浪费了吗?
  2. 单线程模型不是 JS 的专属吗?Java/Go/Rust 里有对应物吗?为什么 Netty EventLoop / Go runtime 的 P 也被称为"单线程亲缘性"?
  3. Redis 6.0 引入多线程 IO 是不是承认单线程错了?

# 1.2 通用本质:单线程不是"省着用 CPU",是"省着用同步"

抛开所有语言关键字,单线程模型的最小定义只有一句:

把一份"被保护的状态"绑定到唯一一个执行单元上,让所有访问该状态的代码串行化执行,从而消除一切同步开销。

注意这里的关键词是"被保护的状态",不是"整个进程"。这就是单线程模型最大的认知陷阱——

误解 真相
"Redis 是单线程的" 是 执行命令的核心循环 单线程;持久化、AOF、网络 IO 都有自己的线程
"Node 是单线程的" 是 JS 执行栈 单线程;libuv 底下有 4 个 worker 线程 + 内核 epoll
"Nginx 是单线程的" 是 每个 worker 进程内 单线程;进程数=核数
"JS 是单线程的" 是 每个 Tab/Worker 内的 JS 上下文 单线程

→ 任何"单线程模型"本质都是"单写者模式":让状态有且仅有一个写者,所有需要修改它的操作都排队到那个写者上。

# 1.3 通用价值:单线程换来什么、付出什么

通用收益 通用代价
无锁 → 吞吐峰值高(无锁竞争开销) 单核瓶颈 → 受限于一个核的频率
顺序确定 → 状态机/事件回放天然可行 长任务(CPU 密集 / 大对象序列化)会拖死整体
调试简单 → 没有竞态,一个堆栈就能复盘 多核要靠"多个单线程协作"(多进程 / 多 Reactor / 多 P)
心智成本低 → 写代码不用想锁 阻塞调用是头号反模式

# 1.4 跨语言"单线程"的对应物(不仅 JS!)

多核时代的真相是:高性能系统都在用"N 个单线程 + 各自负责一份状态"的多 Reactor 模型。

语言 / 系统 "单线程"的形态 多核扩展方式
C / Redis 核心命令循环 单 event loop 线程 Redis Cluster(多进程分片)
C / Nginx worker 每个 worker 进程单线程 多 worker 进程(数量=CPU 核数)
C / DPDK / SPDK 每个核绑定一个 lcore RSS 网卡多队列 → 多 lcore 平行
C++ / 游戏服务器 单逻辑线程 + 多 IO 线程 多 zone / 多 shard 进程
Java / Netty EventLoop 一个 NioEventLoop = 一个线程 + 一个 Selector 多个 EventLoop 组成 EventLoopGroup
Java / Vert.x Verticle 默认绑定到一个 event-loop 线程 多 Verticle 实例 + 分发策略
Java / Akka Actor 每个 Actor 串行处理邮箱消息 多 Actor 实例,调度器分发到多线程
Go / runtime P 每个 P 在某一时刻只跑一个 goroutine 多 P(默认 = GOMAXPROCS)
Rust / tokio current-thread 单线程 runtime tokio multi-thread runtime
Node.js / 主线程 单 JS 上下文 + libuv 事件循环 Cluster 多进程 / Worker Threads
.NET / async UI/HTTP 请求绑定到 SyncContext ThreadPool 多线程 + 上下文绑定

关键认知:

没有"全局单线程"的高性能系统——只有"单线程亲缘性"(thread affinity):把同一份状态的所有操作锁定到同一个线程上。这是单线程模型在工业界的真实形态。

# 1.5 通用方法论:什么时候该选单线程?

通用判断三条:

  1. 状态共享面广 → 单线程消除同步开销,比多线程加锁更快;
  2. 请求处理是 IO 密集型(等数据库、等网络) → 单线程 + 事件循环可以处理上万并发;
  3. 业务要求严格顺序(交易撮合、状态机、游戏逻辑帧) → 单线程是天然解;

反之,**CPU 密集(加密 / 压缩 / 编解码)**永远要丢到其他线程,否则单线程模型立刻退化。

# 1.6 七字真言(贯穿全篇)

🗝️ 一线 · 一序 · 一切静

  • 一线:一个执行单元绑定一份状态(不一定是整个进程,可以是 EventLoop / Actor / Goroutine on P);
  • 一序:所有操作串行排队执行,顺序由进入队列的时间唯一决定——这给状态机 / 回放 / 调试带来巨大红利;
  • 一切静:消除一切同步原语——无锁、无 CAS、无内存屏障、无竞态——这是单线程模型最强的副作用。

三字一组、互为前提:一线不固定就有竞态、一序不保证就要加锁、一切不静就还是多线程。

# 1.7 本篇通用收益(与你用什么语言无关)

读完本篇你将获得三个语言无关的能力:

  1. 看到任何高性能系统都能识别它的"单线程亲缘性"边界——Redis 的 IO 线程不是核心、Netty 的 handler 不能 sleep;
  2. 能在 C/C++/Java/Go/Rust 里写出符合单线程模型的代码——不论语言是否原生支持;
  3. 能判断一段业务该不该上单线程——而不是被"Redis 单线程很快"这种结论误导。

# 02.概述与背景

# 2.1 单线程模型理念

单线程模型是一种基于协作式任务切换的并发编程范式,它通过在单个线程内实现任务的协作式调度,替代了传统的抢占式多线程模型。这种设计思想的核心在于:用协作代替竞争,用调度代替抢占。

# 2.2 多线程模型挑战

# 2.3 要解决的问题

问题领域 传统方案 单线程模型方案 优势
并发处理 多线程+锁 协程+事件循环 无锁、高效
I/O密集型 线程池+阻塞I/O 异步I/O+回调 资源利用率高
内存管理 每线程独立栈 共享堆+协程栈 内存效率优化
错误处理 异常跨线程传播 统一错误处理 简化异常管理
调试测试 不确定性执行 确定性执行 可重现性好

# 03.认识单线程模型

# 3.1 餐厅厨房比喻

想象一个餐厅厨房,有三种工作模式:

工作模式 工作方式 对应编程模型 缺点
多线程厨房 每个订单配一个厨师+一套厨具(线程)。10个订单需要10个厨师,成本高,厨师间容易碰撞(资源竞争)。 多线程 创建/切换线程开销大,易发生竞态条件,需要复杂的锁机制。
单线程阻塞厨房 只有一个厨师。接到订单后,厨师必须一直守着锅直到水烧开,期间不能做任何事。 同步阻塞 效率极低,CPU大部分时间在空闲等待。
单线程+事件循环厨房(协程) 一个高效厨师。烧水时不等待,而是去切菜;等水烧开的等待时间用来炒菜。 协程/异步 需要精心设计任务拆分,所有任务不能有长时间阻塞操作。

第三种模式就是协程的核心思想:充分利用等待时间。

# 3.2 单线程模型

单线程模型是指:程序只有一个主执行线程。代码一行接一行地顺序执行。

同步执行:代码按照顺序逐行执行,前一行代码执行完毕后,才会执行下一行。

阻塞问题:如果某段代码执行时间过长(如复杂的计算或同步 I/O 操作),会阻塞后续代码的执行,导致页面卡顿或无响应。

关键挑战:遇到阻塞问题后,在传统的同步阻塞模型中,线程会停下来傻等,导致CPU资源被白白浪费。

// 传统同步阻塞模型(低效)
console.log('开始点餐');
let data = readFileSync('menu.txt'); // 线程在这里阻塞,直到文件读完
console.log('菜单是:', data);
cookFood(); // 直到上面完成后才执行

解决方案:异步非阻塞 + 事件循环。

# 3.3 单线程核心思想

单线程模型的设计思想体现了以下核心理念:

  1. 协作优于竞争: 通过协作式调度避免线程间的资源竞争
  2. 事件驱动优于轮询: 基于事件的响应式编程模型
  3. 异步优于同步: 非阻塞I/O最大化系统吞吐量
  4. 简单优于复杂: 单线程执行模型简化了并发编程的复杂性

# 3.4 单线程挑战

单线程模型在处理耗时任务时存在明显的局限性:

  • 阻塞问题:如果主线程被长时间占用,页面会失去响应。
  • 性能瓶颈:无法充分利用多核 CPU 的优势。

# 04.核心设计思想

# 4.1 协作式VS抢占式

为什么取名"协作"和"抢占"?这不是名词差异、是哲学差异。

探索:抢占式在哪里“贵”?

抢占式调度是个双刃剑。看似公平(人人有份),但代价是三处隐藏成本:

成本一:内核介入
  每次调度必须陷入内核态、保存全部寄存器、恢复另一个线程的全部寄存器
  开销约 1~5 μs、CPU 周期坐满 1500~10000 个

成本二:缓存失效
  L1/L2/L3 缓存、TLB 都是为“当前在跑的线程”准备的
  切换后全部作废、需要重新加载代价高达 100~500 个周期

成本三:同步复杂度
  你不知道最年什么时候被亝出 CPU
  读-改-写三步随时被中断、只能请劳锁/CAS保护

协作式为什么“快”?

协作式是个“你说你让出、谁都不会强制你让出”的哲学。这意味着:

  零锁:读-改-写间不能被中断,本身就是原子的
  零上下文切换:仅在“让出点”保存必要状态、开销 ~100~500 ns
  高缓存友好度:CPU 始终在同一任务上跑,缓存本服赍闱

代价:必须保证任务不会犬頁"忘记让出"

一个任务跑 5 秒不让出,其他任务全部占充 5 秒。这是单线程模型最大的工程陕阱、也是事件循环生态中必须提供“Worker Pool”的原因。

# 4.2 协作式调度思想

核心机制

// 协作式调度的核心抽象
public abstract class Coroutine {
    private CoroutineState state = CoroutineState.CREATED;
    private Object yieldValue;
    
    // 协程的执行入口
    public abstract void run();
    
    // 主动让出控制权
    protected void yield() {
        this.state = CoroutineState.YIELDED;
        // 保存当前执行状态
        saveExecutionContext();
        // 切换到调度器
        Scheduler.getInstance().schedule();
    }
    
    // 等待异步操作完成
    protected void await(Future<?> future) {
        this.state = CoroutineState.WAITING;
        future.onComplete(() -> {
            this.state = CoroutineState.READY;
            Scheduler.getInstance().wakeup(this);
        });
        yield();
    }
    
    // 恢复执行
    public void resume() {
        this.state = CoroutineState.RUNNING;
        restoreExecutionContext();
        run();
    }
}

状态转换模型

# 05.协程实现多任务

# 5.1 并发VS并行

这是并发领域最容易被混淆、也是面试最常考的两个词。

一句话说清:

并发(Concurrency) = 多个任务同时“进行中”(not necessarily 同时跳动)
并行(Parallelism)  = 多个任务同时“跳动中”

Rob Pike 的经典比喻:

并发 = 你同时能处理多件事(处理过程重叠)
并行 = 你同时能上多件事(同一时刻多件事都在跳动)

例子:你一个人同时烧菜、烧饭、煤面——这是并发(你只有一双手)
夫妻两人一个烧饭一个烧菜——这是并行(两双手同时跳)

单线程模型一定是并发、不是并行:

  100万 个请求同时“进行中”  → 并发能力顶顶高
  但同一时刻只能某个请求“跳” → 并行能力 = 1

这让人起个反直觉:单线程模型能跑赢多线程,不是因为它“同时跳多件事”,而是因为它“万件事同时“不跳””。IO 等待期间可以源源不断”推进”别人、不需要多线程。

CPU密集 为什么必须多线程?

  IO 密集:99% 时间在“等”、只需几个钉的人去提交
  CPU 密集:99% 时间在“跳”、需要多个人同时跳

这就是 Node.js / Redis / Nginx 都提供 Worker Pool / Worker 进程的原因。混合使用“事件循环 + 多 Worker”能同时赢两项。

# 5.2 任务调度器设计

public class CoroutineScheduler {
    private final Queue<Coroutine> readyQueue = new LinkedList<>();
    private final Set<Coroutine> waitingSet = new HashSet<>();
    private Coroutine currentCoroutine;
    
    // 事件循环:单线程多任务的核心
    public void eventLoop() {
        while (!readyQueue.isEmpty() || !waitingSet.isEmpty()) {
            // 1. 处理就绪的协程
            if (!readyQueue.isEmpty()) {
                currentCoroutine = readyQueue.poll();
                currentCoroutine.resume();
            }
            
            // 2. 检查等待中的I/O操作
            checkIOCompletion();
            
            // 3. 处理定时器事件
            processTimerEvents();
            
            // 4. 如果没有就绪任务,等待I/O事件
            if (readyQueue.isEmpty()) {
                waitForIOEvents();
            }
        }
    }
    
    // 协程主动让出时的调度逻辑
    public void yield(Coroutine coroutine) {
        if (coroutine.getState() == CoroutineState.YIELDED) {
            readyQueue.offer(coroutine);
        } else if (coroutine.getState() == CoroutineState.WAITING) {
            waitingSet.add(coroutine);
        }
        // 继续事件循环,执行下一个任务
    }
}

# 5.3 多任务执行时序图

# 5.4 协程栈管理

栈切换机制

栈内存优化

public class CoroutineStack {
    private static final int DEFAULT_STACK_SIZE = 64 * 1024; // 64KB
    private static final Stack<CoroutineStack> stackPool = new Stack<>();
    
    private byte[] stackMemory;
    private int stackPointer;
    private boolean inUse;
    
    // 栈池化管理,减少内存分配
    public static CoroutineStack allocate() {
        synchronized (stackPool) {
            if (!stackPool.isEmpty()) {
                CoroutineStack stack = stackPool.pop();
                stack.reset();
                return stack;
            }
        }
        return new CoroutineStack(DEFAULT_STACK_SIZE);
    }
    
    public static void deallocate(CoroutineStack stack) {
        synchronized (stackPool) {
            if (stackPool.size() < MAX_POOL_SIZE) {
                stack.inUse = false;
                stackPool.push(stack);
            }
        }
    }
    
    // 栈上下文保存和恢复
    public void saveContext(CoroutineContext context) {
        context.stackPointer = this.stackPointer;
        context.registers = getCurrentRegisters();
    }
    
    public void restoreContext(CoroutineContext context) {
        this.stackPointer = context.stackPointer;
        setCurrentRegisters(context.registers);
    }
}

# 06.多请求设计原理

# 6.1 请求处理架构

# 6.2 请求顺序机制

FIFO队列保证

public class RequestProcessor {
    private final Queue<Request> requestQueue = new ConcurrentLinkedQueue<>();
    private final Map<String, Queue<Request>> sessionQueues = new ConcurrentHashMap<>();
    
    // 全局请求顺序保证
    public void processRequest(Request request) {
        requestQueue.offer(request);
        wakeupEventLoop();
    }
    
    // 会话级别顺序保证
    public void processSessionRequest(Request request) {
        String sessionId = request.getSessionId();
        sessionQueues.computeIfAbsent(sessionId, k -> new LinkedList<>())
                    .offer(request);
        wakeupEventLoop();
    }
    
    // 事件循环中的顺序处理
    public void eventLoop() {
        while (running) {
            // 1. 处理全局队列(FIFO顺序)
            Request globalRequest = requestQueue.poll();
            if (globalRequest != null) {
                processInCoroutine(globalRequest);
            }
            
            // 2. 处理会话队列(每个会话内部FIFO)
            for (Queue<Request> sessionQueue : sessionQueues.values()) {
                Request sessionRequest = sessionQueue.poll();
                if (sessionRequest != null) {
                    processInCoroutine(sessionRequest);
                    break; // 轮询处理,保证公平性
                }
            }
            
            // 3. 处理I/O完成事件
            processIOCompletion();
        }
    }
}

# 6.3 优先级队列设计

# 6.4 并发请求处理策略

public class ConcurrentRequestHandler {
    private final CoroutineScheduler scheduler;
    private final Map<String, RequestContext> activeRequests = new ConcurrentHashMap<>();
    
    // 并发处理多个请求
    public void handleRequest(Request request) {
        String requestId = request.getId();
        RequestContext context = new RequestContext(request);
        activeRequests.put(requestId, context);
        
        // 创建协程处理请求
        Coroutine coroutine = new Coroutine() {
            @Override
            public void run() {
                try {
                    // 业务逻辑处理
                    processBusinessLogic(request);
                    
                    // 可能的I/O操作
                    if (needDatabaseAccess(request)) {
                        DatabaseResult result = await(database.queryAsync(request.getQuery()));
                        request.setDatabaseResult(result);
                    }
                    
                    if (needNetworkCall(request)) {
                        NetworkResponse response = await(httpClient.getAsync(request.getUrl()));
                        request.setNetworkResponse(response);
                    }
                    
                    // 生成响应
                    Response response = generateResponse(request);
                    sendResponse(response);
                    
                } finally {
                    activeRequests.remove(requestId);
                }
            }
        };
        
        scheduler.schedule(coroutine);
    }
    
    // 请求取消处理
    public void cancelRequest(String requestId) {
        RequestContext context = activeRequests.get(requestId);
        if (context != null) {
            context.cancel();
            activeRequests.remove(requestId);
        }
    }
}

# 07.事件循环机制设计

# 7.1 事件循环核心架构

事件循环不是一个“队列”、而是四个队列联动的复合设计。

探索:为什么要区分宏任务与微任务?

这是个被必须重看到过、但几乎没人讲过“为什么”的陕阱。考虑以下场景:

// 场景:Promise 链中间被 setTimeout 插足
async function logIn() {
    const user = await fetchUser();          // 微任务返回
    const profile = await fetchProfile();    // 微任务返回
    showWelcome(profile);
}

如果没有微任务优先、仅有宏任务队列:

  fetchUser 结束、then 回调进任务队列
  “这时中间什么都可能插进来”:setTimeout 起、UI 事件响、IO 事件响
  用户看到:“profile 动画被动画生活代进上变问”

微任务的理论作用:保证一条 Promise 链能一口气跑完、不被外部事件切走。这个体验上就是“交互资体”:不会动画会中间被其他事件插一脚、不会出现“半个 UI 表现”。

五层优先级(Node.js libuv 底层骑以):

   1) 同步代码             → 优先级最高
   2) 微任务(process.nextTick / Promise.then)
   3) Timer 阶段(setTimeout / setInterval)
   4) Pending callbacks 阶段(上个 tick 剩的)
   5) Poll 阶段(IO 事件回调)
   6) Check 阶段(setImmediate)
   7) Close 阶段(关闭回调)

Node.js vs 浏览器区别:浏览器只区分宏/微任务两类,Node.js 有五个阶段 + 微任务。这是面试重点陕阱点。

# 7.2 事件循环执行流程

# 7.3 非阻塞I/O设计

“非阻塞 IO” 不是一个词、是四种不同的意思。要看懂底层、必须先理清这四种。

探索:IO 模型的四种状态

结合实例说明:

// 同步阻塞 —— 你在店里当面等菜、不能走
bytes = read(fd, buf, 1024);   // 阻塞 5 秒

// 同步非阻塞 —— 问问菜好了没、没好就先走、一下就问
fcntl(fd, F_SETFL, O_NONBLOCK);
while ((bytes = read(fd, buf, 1024)) == -1 && errno == EAGAIN) {
    do_other_stuff();    // 问他考事、但你还是要不断轮询
}

// 异步阻塞 —— 告诉店员“菜好了叫我”、你去别处等叫
select(maxfd+1, &readfds, NULL, NULL, NULL);   // 谁好了、叫你
for (fd = 0; fd <= maxfd; fd++) {
    if (FD_ISSET(fd, &readfds)) read(fd, ...);
}

// 异步非阻塞 —— 告诉店员“菜好了、你出去送到我桌子上、我只需拿就劣”
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, len, offset);
io_uring_submit(&ring);
// IO 完成后、内核把结果直接仅出在 CQ、你 wait 一下就拿到

主流事件循环用哪种?

libuv (Node.js)   → 二层、Linux 用 epoll、Windows 用 IOCP
Redis ae          → 与 libuv 类似、选二层
Nginx Worker      → 直接用 epoll
Go runtime        → netpoll 上封装 epoll/kqueue/IOCP
Tokio (Rust)       → mio 上封装 epoll、可选 io_uring
public class NonBlockingIOManager {
    private final Selector selector;
    private final Map<SelectionKey, IOCallback> callbacks = new HashMap<>();

    public Future<ByteBuffer> readAsync(SocketChannel channel) {
        CompletableFuture<ByteBuffer> future = new CompletableFuture<>();
        try {
            channel.configureBlocking(false);
            SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
            callbacks.put(key, k -> {
                ByteBuffer buf = ByteBuffer.allocate(1024);
                int n = channel.read(buf);
                buf.flip();
                future.complete(buf);
            });
        } catch (IOException e) { future.completeExceptionally(e); }
        return future;
    }

    // 事件循环中调用
    public void pollIOEvents(long timeoutMs) throws IOException {
        if (selector.select(timeoutMs) == 0) return;
        Set<SelectionKey> keys = selector.selectedKeys();
        for (SelectionKey k : keys) callbacks.remove(k).onReady(k);
        keys.clear();
    }
}

如果是同步非阻塞、你就在 提问轮询 —— 这不比同步阻塞快。只有加上 IO 多路复用、“不轮询、被通知” 的变革才发生。

# 08.实际应用分析

# 8.1 协程VS线程

特性 线程 (Thread) 协程 (Coroutine)
调度方式 操作系统内核抢占式调度 用户态协同式调度(主动让出)
创建开销 较大(需分配MB级栈内存) 极小(通常KB级)
切换成本 高(需内核介入,保存所有寄存器) 极低(只需保存少量上下文)
数量上限 千级(受内存限制) 百万级(轻松创建)
并发模型 并行(多核)或并发(单核) 并发(单线程内交替执行)
编程复杂度 高(需要锁、同步机制) 低(类似同步代码的写法)

# 8.2 实际应用场景

适合协程的场景(I/O密集型)

  • Web服务器:处理大量HTTP请求(大部分时间在等待网络I/O)
  • 爬虫程序:同时抓取多个网站
  • 聊天服务器:管理大量并发连接
  • 数据库操作:等待查询结果

不适合协程的场景(CPU密集型)

  • 视频编码:需要大量计算,几乎没有I/O等待
  • 科学计算:持续占用CPU
  • 图像处理:计算密集型任务

对于CPU密集型任务,应该使用:多线程 + 协程(如Go语言的goroutine,会自动在多核上调度)

# 8.3 单线程模型的最佳实践

1. 避免阻塞操作

单线程模型中,任何阻塞操作都会导致整个系统停滞。关键原则:

  • 所有I/O操作必须使用非阻塞API
  • 计算密集型任务拆分为多个小任务,穿插执行
  • 使用任务队列解耦生产者和消费者

2. 合理使用定时器

  • 避免使用精确定时器做频繁的轮询
  • 利用事件驱动替代定时轮询
  • 注意定时器回调中的异常处理

3. 错误处理策略

单线程中未捕获的异常可能导致整个线程崩溃:

  • 关键操作使用 try-catch 保护
  • 实现全局异常处理器
  • 日志记录和监控告警

4. 性能监控

  • 监控事件循环的耗时,检测长任务
  • 统计消息队列长度,发现积压问题
  • 追踪关键路径的执行时间

# 09.跨语言模型对比

单线程 + 事件循环的思想被多语言反复实现,但每个语言都有自己的实现哲学。

# 9.1 五大单线程架构上台

# 9.2 跨语言实现抹出

实现 事件循环名 I/O 多路复用 适用场景 并发能力
JavaScript (V8) Microtask + Macrotask 无外部 IO(浏览器代理) 浏览器 UI 单机一核低到中等
Node.js libuv epoll/kqueue/IOCP 服务器 / CLI 单机高并发
Redis 自研 ae epoll/kqueue/select 内存数据库 10 万 QPS
Nginx Worker 自研 epoll HTTP 反代 10 万连接
Dart (Flutter) Isolate 事件循环 platform channel 跨平台 UI UI 连贯

# 9.3 Redis单线程能跑100万QPS?

这是个典型的反直觉问题。拆开这 100 万 QPS 背后的六个设计决策:

决策一:纯内存 → 一条命令 几十微秒,不是几毫秒
决策二:IO 多路复用 → 一个线程看完所有连接
决策三:单线程命令 → 零锁、零上下文切换、零缓存作废
决策四:高效数据结构 → SDS、ziplist、listpack 都是内存优化后的产物
决策五:多路复用选 epoll → 100万 fd 仅一次 epoll_wait
决策六:高效协议 → RESP 语言不反序列化、仅“看一下”

结果:单个命令 ≈ 0.5 微秒 → 100 万 QPS 是物理上限。不是"单线程难能可贵",是"多线程加上去不妨能提高吗"——Redis 6 引入的仅仅是 IO 多线程、命令执行仍是单线程。

# 9.4 Nginx Worker隐藏双层设计

Nginx 是个低调的架构抹点:

核心设计哲学:

  • 外层多进程:充分利用多核、进程间完全隔离(某个 Worker 崩溃不影响其他)
  • 内层单线程:避免锁、最大化单个进程处理能力

这个“多进程 * 单线程”架构是高并发服务器的黄金设计——Apache 的 “一连接一进程”在同类场景下被严重限制。

# 10.经典陷阱与反模式

单线程看似简单,但有五个典型的陷阱不踩不知道。

# 10.1 陷阱①|CPU密集任务阻事件循环

// ❌ 在 Node.js 主线程跑大计算
const data = computePrimes(1_000_000);   // 5 秒同步计算
// 这 5 秒里所有 HTTP 请求都被让着、服务实际上停了

// ✅ 拆到 Worker Thread
const { Worker } = require('worker_threads');
const worker = new Worker('./compute.js', { workerData: 1_000_000 });
worker.on('message', data => { /* 主线程仍响应 */ });

根因:事件循环的哲学是"推迟为二、仅计算为一"——计算最快、是占用者。占用多久、服务就被护多久。

# 10.2 陷阱②|微任务饱饱

// ❌ 微任务递归 → 宏任务永远收不到
function loop() {
    Promise.resolve().then(loop);   // 递归调微任务
}
loop();
// setTimeout 、 setInterval 、 IO 回调、UI 事件 全部被饱饱

根因:JavaScript 事件循环是"微任务饱饱后才跳到宏任务"——你一直递归调微任务,其他什么都收不到。

# 10.3 陷阱③|隐式跨事件循环依赖

// ❌ 跨事件循环保存状态
let counter = 0;
async function handler(req, res) {
    counter++;            // 读-改-写三步被 await 打断
    await fetch(...);
    counter--;
    res.send(counter);   // 你看到的 counter 是谁留下的?
}

根因:虽然是单线程,但 await 是个让出点。顶多股能同时进入 handler,变量表现为“补际交错”、不是并发错误,但同样令人难以理解。

# 10.4 陷阱④|内存泄漏于闭包

const handlers = [];
function registerOnce(data) {
    handlers.push(() => process(data));   // 闭包依赖 data、永不释放
}

根因:单线程里未释放的闭包是隐式内存泄漏。这里别人主动调劳动套,data 永不被 GC。Chrome DevTools 的 Heap Snapshot 可调查。

# 10.5 陷阱⑤|错误一步令服务崩溃

// ❌ Node.js 主线程未捕获异常 → 整个进程退出
process.on('uncaughtException', (err) => {
    log.error(err);
    process.exit(1);   // 这是谁都推荐的选择、全场隐藏状态丢弃
});

根因:单线程意味着 1 个未捕获异常 = 全部服务崩溃。必须在所有可能错误点插入 try/catch、Promise.catch、以及进程级别的 uncaughtException 兜底,然后推荐反转进程刷出纯净状态。


# 11.跨语言单线程陷阱速查表

同一个坑、不同方言——下表每一行是一种语言在单线程亲缘性模型下"最容易翻车"的反模式,按七字真言一线/一序/一切静三象限分类。

语言 / 系统 经典陷阱 触犯真言 一句话避坑
JS / Node 长循环(大数组 JSON 序列化)卡死事件循环 一线 拆批 + setImmediate 让出,或丢 worker_threads
JS / Node 顶层 await 阻塞冷启动 一序 顶层只用同步,异步初始化放进 ready 钩子
C / Redis 大 key 操作(KEYS * / 删 100MB hash)阻塞命令循环 一线 用 SCAN 增量、UNLINK 异步释放
C / Redis Lua 脚本里 while true 死循环 一线 必设 lua-time-limit,长任务用 Redis Function
C / Nginx worker 内调阻塞 syscall(同步 DNS、同步 gethostbyname) 一线 用 ngx_resolver 异步 DNS,禁同步 IO
C++ / 游戏服 主逻辑线程做加密 / 压缩 / 大 JSON 一线 丢工作线程池,主线程只做"状态变更"
C++ / 游戏服 主线程调 SQL 同步驱动 一线 改用异步 DB 客户端或专用 DB 线程
Java / Netty EventLoop handler 内 Thread.sleep / JdbcTemplate.query 一线 长任务丢业务 EventExecutor 或 taskExecutor
Java / Vert.x Verticle handler 内调阻塞代码 一线 用 executeBlocking 或 WorkerVerticle
Java / Akka Actor 内 Await.result 等子 Actor 回复 一序 改用 ask + 后续消息驱动,不能阻塞自己
Go / runtime P CGO 阻塞调用占住 P → 调度抖动 一线 用专用 OS 线程 / sysmon 监控 / 缩短调用
Go / runtime P 大量 runtime.LockOSThread 不释放 一线 完事必须 Unlock,否则 P 数被吃光
Rust / tokio current-thread 同步 std::thread::sleep 阻塞整个 runtime 一线 改 tokio::time::sleep 或 spawn_blocking
.NET / async UI 线程 .Result / .Wait() 死锁 一序 UI 全程 await,库代码用 ConfigureAwait(false)
Python / asyncio event loop 内调 requests / time.sleep 一线 用 aiohttp / asyncio.sleep 异步原语
任意语言 未捕获异常逃出事件循环 → 整服崩溃 一切静 顶层 catch + 进程级 uncaughtException 兜底重启

# 12.七字真言 × 多语言映射表

🗝️ 一线 · 一序 · 一切静——下表把每一字诀映射到各语言/系统的具体形态,任何高性能系统都能用这三字诀拆开。

真言 C/Redis C/Nginx Java/Netty Java/Akka Go Rust/tokio Node.js
一线
(单写者)
单 event loop 线程 单 worker 进程内单线程 一个 NioEventLoop=一线程 每个 Actor 串行 一个 P 同时只跑一个 g current-thread runtime 主线程跑 JS
一序
(操作排队)
命令队列 FIFO accept 队列 + 处理队列 EventLoop taskQueue Actor mailbox runq 本地队列 tokio task queue task / microtask queue
一切静
(零同步)
命令路径零锁 进程内无锁 handler 内零锁 actor 内零锁 g 内访问本 P 数据无锁 task 内访问 !Send 数据无锁 JS 闭包内无锁

通用记忆口诀:

"一线保证只有一个写者——所有共享状态都被它独占; 一序保证操作顺序确定——状态机和回放天然可行; 一切静是前两条的副产品——没有了竞态,锁就没意义了。"

反例识别:任何在 handler / 回调 / 命令路径 / Actor 体内出现 lock / mutex / synchronized / Atomic* 的代码,都是对单线程模型的违背——要么状态边界划错了,要么有人偷渡了多写者。


# 13.一句话总结

单线程模型的本质,是用“物理上不走过摆”换“逻辑上可推理”。

三层认知:

表层抽象:只有一个线程,多任务靠合作式调度 —— 看起来慢
中层抽象:事件循环 + 非阻塞 IO,让“等待”不占用主线程 —— 实际能跑出 10万 QPS
底层抽象:零锁、零上下文切换、硬件缓存友好、确定性可调试 —— 这才是它能跑赢多线程的本质原因

终极建议:

  • **IO 密集场景(网关、缓存、反代、脚本语言运行时)**→ 单线程是首选;
  • CPU 密集场景(视频转码、加密、计算) → 多线程/多进程/Worker Pool;
  • 混合场景 → 主线程调度 + Worker Pool 干重活,这是 Node.js / Nginx / Redis 6 都走上的路。

# 📎 延伸阅读

  • 前一篇:21.异步和同步的设计 (opens new window)(单线程的前提是异步 IO)
  • 下一篇:23.协程核心设计思想 (opens new window)(单线程中“任务”的本质是协程)
  • 范式分流:24.Actor与CSP并发模型 (opens new window)(单线程 + 消息传递 = 最干净的并发模型)
  • 对照阅读:15.并发编程设计思想 (opens new window)(多线程范式的全谱)
上次更新: 2026/06/28, 17:55:19
11.异步和同步的设计
13.协程核心设计思想

← 11.异步和同步的设计 13.协程核心设计思想→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式