从高级语言的语法糖,到操作系统内核的调度算法 — 彻底搞懂计算机世界里的"同时做多件事"到底是怎么一回事。
操作系统线程是主角。Java ThreadPool、Go goroutine、C++ std::thread 都属于这一类。
Thread-based用户态"轻量级线程",不阻塞 OS 线程。Python asyncio、JavaScript Promise、Rust tokio、C# async/await。
Async/Await单线程 + IO 多路复用。Node.js、nginx、Redis、Vert.x 都靠这个打死高并发。
Event-driven这两个词经常被混用,但它们的区别是整个话题的基石。
逻辑上同时,物理上可能交替。就像一个人同时处理三件事 — 聊天、写文档、回邮件。每件事切一小段时间轮流做,宏观上感觉像同时在干。单核 CPU 完全可以并发。
物理上真正同时执行。需要多个 CPU 核心。就像一个团队三个人分别做三件事,没有任何时间轮转。并行是并发的子集 — 并行一定并发,并发不一定并行。
| 模型 | 调度单位 | 谁在调度 | 上下文切换成本 | 典型代表 | 适合场景 |
|---|---|---|---|---|---|
| 多线程/线程池 | OS 线程 (Thread) | 操作系统内核(抢占式调度) | 高 (~1-10μs, 涉及内核态切换) | Java ThreadPool, C++ std::thread, Go goroutine | CPU 密集型计算、需要真正并行的任务 |
| 异步/协程 | 协程 (Coroutine/Fiber) | 语言运行时/用户态(协作式调度) | 极低 (~几十ns, 纯用户态) | Python asyncio, JS async/await, Rust tokio, Kotlin coroutine | 高并发 IO(网络请求、数据库查询) |
| 事件驱动 | 事件 (Event/Callback) | Event Loop + IO 多路复用 | 几乎为零 (单线程,无切换) | Node.js, nginx, Redis, libuv, Netty | 海量短连接、IO 密集型服务 |
所有并发模型都在解决同一个问题:当一个线程因为等待 IO 而空闲时,CPU 不该闲着。
假设你有一个 Web 服务器,每个请求需要读数据库(100ms),CPU 处理只要 1ms。如果用传统的"一个线程一个请求"模型,处理 10000 个并发请求就需要 10000 个线程,每个线程 99% 的时间都在等 IO — 大量内存和上下文切换开销被浪费了。
三大模型用不同思路解决这个问题:
线程模型是最"传统"的并发方式。每个线程都有自己的栈空间和寄存器上下文,由操作系统内核统一调度。关键问题是:如何管理线程的创建与销毁? 于是演化出了几种经典模式。
最简单粗暴的做法。Apache HTTP Server (prefork/worker) 早期就是这么干的。问题是线程创建/销毁开销大,大量线程吃内存。
// ❌ 简单但不实用的写法
class SimpleServer {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(8080);
while (true) {
Socket client = server.accept();
// 为每个连接创建一个新线程
new Thread(() -> {
handleRequest(client); // 处理请求
}).start();
}
}
}
这才是生产环境的标配。预先创建固定数量的线程,任务以队列方式提交,线程复用。避免了频繁创建销毁线程的开销,同时限制了线程总数。
import java.util.concurrent.*;
// ✅ 生产级线程池
ThreadPoolExecutor pool = new ThreadPoolExecutor(
4, // corePoolSize: 核心线程数
8, // maximumPoolSize: 最大线程数
60L, TimeUnit.SECONDS, // 空闲线程存活时间
new LinkedBlockingQueue<>(100), // 任务队列容量
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 提交任务
Future<String> future = pool.submit(() -> {
// CPU 密集型或 IO 操作
return fetchFromDatabase(id);
});
// 获取结果(阻塞等待)
String result = future.get(5, TimeUnit.SECONDS);
// 优雅关闭
pool.shutdown();
线程池虽然好,但也有天花板。假设每个线程默认栈空间 1MB,1000 个线程就是 1GB 内存。而且大量线程会导致频繁的上下文切换 — 操作系统在多个线程之间来回切换的代价不低(保存/恢复寄存器、刷新 TLB 等)。这就是为什么纯线程模型处理 10000+ 并发连接会吃力。
CPU 密集型任务。比如图像处理、加密解密、科学计算 — 这些任务很少等待 IO,线程基本满负荷跑。核心线程数设为 CPU 核心数即可。
不适合:纯 IO 密集型高并发(10000+ 并发 HTTP 请求)。这种场景下协程或事件驱动更优。
协程 (Coroutine) 的核心思想:把"等待"从内核态搬到用户态。当协程遇到 IO 操作时,不阻塞 OS 线程,而是主动让出 (yield) 执行权,让同一个线程去跑其他协程。IO 完成后,协程从上次暂停的地方恢复执行。
import asyncio
import aiohttp
# async def 定义一个协程函数
async def fetch_url(url):
print(f"开始获取 {url}")
async with aiohttp.ClientSession() as session:
# await 让出执行权,不阻塞线程
async with session.get(url) as resp:
data = await resp.text()
print(f"完成获取 {url}, 长度: {len(data)}")
return data
async def main():
urls = [f"https://api.example.com/item/{i}" for i in range(100)]
# 同时发起 100 个请求,全部在单个线程中完成!
results = await asyncio.gather(*[fetch_url(u) for u in urls])
print(f"全部完成,共 {len(results)} 个结果")
# 运行事件循环
asyncio.run(main())
import kotlinx.coroutines.*
suspend fun fetchUser(id: Int): User {
// suspend 函数可以在不阻塞线程的情况下挂起
return withContext(Dispatchers.IO) {
database.query("SELECT * FROM users WHERE id = $id")
}
}
fun main() = runBlocking {
// 并发请求 1000 个用户,只需要很少的线程
val users = (1..1000).map { id ->
async { fetchUser(id) } // 创建协程,不阻塞
}.awaitAll() // 等待所有完成
println("获取了 ${users.size} 个用户")
}
use tokio;
#[tokio::main]
async fn main() {
let urls = vec!["https://example.com/a", "https://example.com/b"];
// 并发执行多个异步任务
let results: Vec<_> = futures::future::join_all(
urls.iter().map(|&url| tokio::spawn(async move {
let body = reqwest::get(url).await.unwrap().text().await.unwrap();
println!("{} -> {} bytes", url, body.len());
}))
).await;
}
协程切换只涉及几十个 CPU 指令(保存/恢复少量寄存器),无需陷入内核态。对比线程切换需要 1-10 微秒,协程切换只需几十纳秒。
每个协程初始栈通常只有 2-4 KB(如 Go goroutine),需要时再增长。而 OS 线程默认栈 1MB 起步。100 万个协程 vs 100 万个线程,内存差距是天文数字。
async/await 语法让异步代码看起来像同步代码,避免了回调地狱 (callback hell)。写起来直观,调试起来也舒服很多。
网络请求、数据库查询、文件读写 — 大量等待的操作是协程的最佳战场。CPU 密集型计算不是协程的强项。
事件驱动模型最激进的理念:只要程序设计得当,一个线程就够了。核心依赖 IO 多路复用机制(epoll / kqueue / IOCP),一个线程同时监听成千上万个文件描述符,哪个就绪处理哪个。
const http = require('http');
const fs = require('fs');
// 创建 HTTP 服务器 — 所有请求在单个线程中处理
const server = http.createServer((req, res) => {
if (req.url === '/') {
// 异步读文件 — 不阻塞主线程
fs.readFile('data.json', (err, data) => {
res.end(data);
});
} else if (req.url === '/api') {
// 模拟异步数据库操作
fetchDataFromDB().then(result => {
res.end(JSON.stringify(result));
});
}
});
server.listen(3000, () => {
console.log('Server running — 单线程处理成千上万并发');
});
# nginx.conf
worker_processes auto; # worker 数 = CPU 核心数
events {
worker_connections 65535; # 每个 worker 可同时处理 65535 个连接
use epoll; # Linux 下用 epoll
multi_accept on; # 一次 accept 所有新连接
}
http {
server {
listen 80;
# 单 worker 单线程可处理数万并发请求
location / {
proxy_pass http://backend;
}
}
}
单线程意味着不需要任何锁机制,没有上下文切换开销,没有竞态条件。内存占用极低。nginx 单机能扛 100 万并发连接。
如果 Event Loop 中执行了耗时操作(比如一个 CPU 密集计算),所有其他请求都会被阻塞。Node.js 的 worker_threads 和 nginx 的线程池就是为这个场景准备的。
Java 是并发模型最丰富的语言之一,从传统线程到虚拟线程(Project Loom),一条完整的演进路线。
import java.util.concurrent.*;
// Java 21+ 虚拟线程 — 百万级轻量级线程
// 每个虚拟线程映射到少量 OS 线程上
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 一口气提交 100 万个任务
for (int i = 0; i < 1_000_000; i++) {
final int id = i;
executor.submit(() -> {
// 这里的 sleep 不会阻塞 OS 线程
Thread.sleep(Duration.ofMillis(100));
System.out.println("Task " + id + " done");
});
}
}
// 虚拟线程本质是用户态协程,挂载在平台线程上运行
| 机制 | 模型 | 适用场景 | Java 版本 |
|---|---|---|---|
| Thread + Runnable | OS 线程 | 简单并发 | JDK 1.0 |
| ThreadPoolExecutor | 线程池 | CPU 密集、受控并发 | JDK 5 |
| ForkJoinPool | 工作窃取线程池 | 递归分治、并行计算 | JDK 7 |
| CompletableFuture | 异步任务编排 | 多步异步流程 | JDK 8 |
| Virtual Threads | 用户态协程 | 高并发 IO、替代线程池 | JDK 21 |
| Reactor / WebFlux | 事件驱动 + 响应式 | 高吞吐 Web 服务 | Spring 5+ |
Go 的并发模型独树一帜:goroutine + channel,受 CSP (Communicating Sequential Processes) 理论启发。
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for job := range jobs {
fmt.Printf("Worker %d processing job %d\n", id, job)
time.Sleep(100 * time.Millisecond) // 模拟工作
results <- job * 2
}
}
func main() {
jobs := make(chan int, 100)
results := make(chan int, 100)
// 启动 3 个 goroutine 作为 worker 池
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 发送任务
for j := 1; j <= 10; j++ {
jobs <- j
}
close(jobs)
// 收集结果
for a := 1; a <= 10; a++ {
<-results
}
}
Goroutine 本质:Go 运行时实现了 M:N 调度 — M 个 goroutine 映射到 N 个 OS 线程。Goroutine 初始栈只有 2KB,可以动态增长。Go 的调度器 (GMP 模型) 在用户态做抢占式调度,当 goroutine 执行超过 10ms 时会被抢占。
PHP 传统上是"请求-响应"模型,每个请求独立,天然适合无状态。但现代 PHP 也有了多种并发方案:
| 方案 | 模型 | 适用场景 | 成熟度 |
|---|---|---|---|
| PHP-FPM 多进程 | 进程池 | 传统 Web 应用(默认方案) | ⭐⭐⭐⭐⭐ |
| Swoole 扩展 | 事件驱动 + 协程 | 高并发 API、WebSocket、长连接 | ⭐⭐⭐⭐ |
| ReactPHP | 事件驱动(纯 PHP) | 异步 HTTP Client、定时任务 | ⭐⭐⭐ |
| parallel 扩展 | 多线程 | CPU 密集计算 | ⭐⭐ |
| Fiber (PHP 8.1+) | 用户态协程(底层原语) | 框架作者用于构建异步库 | ⭐⭐⭐ |
// Swoole — 基于事件驱动 + 协程的高性能 PHP 服务器
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;
use Swoole\Coroutine;
$server = new Server("0.0.0.0", 9501);
$server->on("request", function (Request $req, Response $res) {
// 协程化 MySQL 查询 — 不阻塞其他请求
$mysql = new Swoole\Coroutine\MySQL();
$mysql->connect(['host' => '127.0.0.1', 'user' => 'root',
'password' => '', 'database' => 'test']);
$data = $mysql->query('SELECT * FROM users LIMIT 10');
// 并发发起多个协程
$results = [];
for ($i = 0; $i < 5; $i++) {
Coroutine::create(function () use (&$results, $i) {
// 这里可以并发请求多个外部 API
$results[$i] = file_get_contents("https://api.example.com/$i");
});
}
$res->end(json_encode($data));
});
$server->start();
Python 有 GIL (Global Interpreter Lock) 的历史包袱,这使得同一时刻只有一个线程能执行 Python 字节码。但 Python 生态发展出了很多绕过 GIL 的方案:
| 方案 | 模型 | 绕过 GIL? | 适用场景 |
|---|---|---|---|
| threading | OS 线程 | ❌ 受 GIL 限制 | IO 密集型(网络、文件) |
| multiprocessing | 多进程 | ✅ 每个进程独立 GIL | CPU 密集型(计算) |
| asyncio | 异步协程 | ✅ 单线程内调度 | 高并发 IO |
| concurrent.futures | 线程/进程池 | 视具体执行器而定 | 简单的并发任务 |
| subinterpreters (3.12+) | 子解释器 | ✅ 独立 GIL | 隔离的并行任务 |
from multiprocessing import Pool, cpu_count
def heavy_computation(n):
"""CPU 密集型计算 — 适合用多进程"""
return sum(i * i for i in range(n))
# 用进程池并行计算
with Pool(processes=cpu_count()) as pool:
results = pool.map(heavy_computation, [10_000_000] * 8)
print(f"完成 {len(results)} 个计算任务")
# 注意:对于 IO 密集型,用 asyncio 或 threading
无论你用的是 Go 的 goroutine 还是 Python 的 asyncio,最终都要落到操作系统层面。这一节从用户态一路走到内核态,讲清楚底层的"真相"。
async/await · go func() · Thread.start() · pool.submit()pthread_create() · epoll_create() · kqueue() · select()sys_clone() / sys_epoll_ctl() / sys_futex()kevent() / mach_thread_create()int 0x80 (x86) / syscall (x86-64) / svc (ARM)
当一个 Java 线程被创建时,背后发生了什么?
传统 select()/poll() 每次调用都要遍历所有文件描述符 — O(n) 复杂度。epoll 用 红黑树 + 就绪链表,把复杂度降到 O(1)。
#include <sys/epoll.h>
#include <unistd.h>
int epoll_fd = epoll_create1(0); // 创建 epoll 实例(内核中分配红黑树+链表)
struct epoll_event ev;
ev.events = EPOLLIN; // 监听可读事件
ev.data.fd = socket_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &ev); // 注册到红黑树
struct epoll_event events[MAX_EVENTS];
while (1) {
// 阻塞等待就绪事件 — 只返回就绪的 fd
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
// 只处理就绪的 fd,O(1) 复杂度!
handle_ready_fd(events[i].data.fd);
}
}
最多监听 1024 个 fd。每次调用都要把整个 fd_set 从用户态拷贝到内核态,内核遍历整个集合。 O(n),已被淘汰。
去掉了 1024 的限制,但还是要遍历整个数组。O(n),比 select 稍好但大规模场景不行。
红黑树存所有 fd,就绪链表存活跃 fd。epoll_wait 只需处理就绪的。 O(1),百万连接不是梦。
BSD 系的事件通知机制,和 epoll 类似的高性能方案。设计上更通用,不仅限于 fd 事件。
IO 完成端口,Windows 的异步 IO 基础设施。线程池 + 完成通知,和 epoll 思路不同但效果类似。
| 维度 | 🧵 线程池 | ⚡ 异步协程 | 🔄 事件驱动 |
|---|---|---|---|
| 调度者 | OS 内核(抢占式) | 语言运行时(协作式) | Event Loop(协作式) |
| 上下文切换 | ~1-10μs(内核态) | ~几十 ns(用户态) | 几乎为零 |
| 内存 (每单位) | ~1MB(线程栈) | ~2-4KB(协程栈) | ~几字节(事件) |
| 单位上限 | 数千 | 数十万 ~ 百万 | 百万级别 |
| 并行能力 | ✅ 真并行(多核) | ⚠️ 需配合多线程 | ⚠️ 需多 worker 进程 |
| 锁需求 | 需要(竞态条件) | 一般不需要 | 不需要 |
| 编程复杂度 | 中等(锁、死锁) | 中等(async/await) | 较高(回调拆分) |
| 调试难度 | 高(竞态、死锁) | 中等 | 中等(调用栈浅) |
| 最适合 | CPU 密集计算 | IO 密集 + 代码可读 | 海量短连接服务 |
| 不太适合 | 海量 IO 并发 | 纯 CPU 计算 | 长耗时 CPU 任务 |
→ 用线程池 (或进程池)
这类任务 99% 时间在跑 CPU,不需要等 IO。线程数设为 CPU 核数即可。Python 用 multiprocessing、Java 用 ForkJoinPool。
→ 用异步协程
大量时间在等数据库返回结果。Go 用 goroutine、Python 用 asyncio + asyncpg、Java 用 Virtual Threads、C# 用 async/await。
→ 用事件驱动
nginx 就是这个场景的王者。或者用 Go 的 net/http(底层也是 epoll),Node.js + cluster 模块也行。
→ 看消息处理特点
纯转发 → 事件驱动。需要复杂业务逻辑 → 协程。需要并发写数据库 + 调 API → 线程池 + 协程混合。
实际的生产系统往往不是单一模型,而是多模型组合:
💡 记住核心公式:协程 = 用户态调度 + 异步 IO,事件驱动 = 单线程 + IO 多路复用,线程池 = OS 线程 + 任务队列
所有高级抽象最终都通向同一条路 — 操作系统内核的调度器和 IO 子系统。