Whenever you have eliminated the impossible, whatever remains,
however improbable, must be the truth.
— Arthur Conan Doyle
photo by Samuel
Field(https://unsplash.com/@s_giovanni?utm_source =templater_proxy&utm_medium=referral)
on Unsplash
第 1 章:并发 vs 并行
1.1 核心定义
并发 (Concurrency)
同一时间处理 (dealing with)许多事情
资源的效用与效率
并行 (Parallelism)
同一时间做 (doing)许多事情
增加 资源投入
多任务 (Multitasking)
同一时间使多个任务取得进展 (progress)
实现方式:并发 或 并行
经典比喻:并发是更聪明地工作;并行是投入更多资源到问题上。
并发(单核交替推进,任务"可中断"): Task A: ████----████------████ Task B: ----████----██████---- 时间轴: ──────────────────────→ (虚线 = 暂停等待) 并行(多核同时执行): Core 1 / Task A: ████████████████ Core 2 / Task B: ████████████████ 时间轴: ────────────────→
1.2 为什么需要并发(两大场景)
db_query (sql, |result| { });loop { heavy_task.run_for (16 .ms); ui.refresh (); }
1.3 关键洞察:参照系(Frame of
Reference)
"同步执行"是一场错觉 ——它只在"你写的代码"这个参照系中成立:
graph TD
subgraph 三层参照系
A["程序员视角<br/>自己的代码按顺序执行<br/>'看起来是同步的'"]
B["OS 视角<br/>抢占式调度<br/>随时暂停/恢复你的线程"]
C["CPU 视角<br/>随时被硬件中断<br/>流水线+乱序执行"]
end
A -->|"其实被"| B -->|"其实被"| C
谈论并发时若不先声明参照系,很快就会陷入混乱——这是理解后文一切的基础。
第 2 章:异步的历史
2.1 演进时间线
graph LR
A["单 CPU<br/>顺序执行"] --> B["非抢占式多任务<br/>(协作式, DOS/Win95)"]
B --> C["抢占式多任务<br/>(OS 负责调度, 现代 OS)"]
C --> D["超线程<br/>1 核模拟 2 逻辑核"]
D --> E["多核处理器<br/>+ 每核超线程"]
2.2 非抢占式 vs 抢占式多任务
fn window_message_loop () { loop { handle_one_event (); yield_to_os (); } }fn my_code () { let x = 1 + 1 ; let y = x * 2 ; }
非抢占式 :调度责任在程序员,一个 bug
拖垮整个系统
抢占式 :OS
每秒多次上下文切换 ,保证 UI/后台任务/I/O
都拿到时间片(现代 OS 的标准设计)
2.3 超线程(Hyperthreading)
一个物理核内有多个运算单元(如 ALU): 线程 1 使用 ALU 时,线程 2 可利用闲置的浮点单元/逻辑单元 → 1 个物理核 = 2 个逻辑核(如 6 核 12 线程) → 额外性能约 +30%(取决于工作负载,自 1990s 持续改进)
2.4
"你的代码有多同步?"——三个视角再回顾
程序/线程视角 :按编写顺序执行 ✔(同步的错觉)
OS 视角 :可能中断、暂停、恢复你的代码
CPU
视角 :流水线(当前指令执行时预取下一条)、分支预测、乱序重排指令 (不告知程序员/OS)——所以"A
发生在 B
之前"在硬件层并无保证,这正是同步原语(mutex/atomic)存在的原因
第 3 章:OS 与 CPU
3.1 OS"假装"同步
OS 自 1990s
以来就"假装"以同步方式执行程序。所谓同步代码只是对程序员看似同步 ;OS
采用抢占式调度,你无法保证代码不被中断地逐条执行。
3.2 系统调用(syscall)——与
OS 通信的唯一正道
你无权直接操作硬件(如网卡),必须通过 syscall 请求 OS 代劳。
三个抽象层次实现同一个功能(向 stdout 输出):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 unsafe { llvm_asm!(" mov $$1, %rax # syscall 号: write mov $$1, %rdi # fd: stdout mov $0, %rsi # 缓冲区地址 mov $1, %rdx # 长度 syscall # 进入内核 " : : "r" (msg_ptr), "r" (len) : "rax" ,"rdi" ,"rsi" ,"rdx" ); }#[cfg(not(target_os = "windows" ))] #[link(name = "c" )] extern "C" { fn write (fd: u32 , buf: *const u8 , count: usize ) -> i32 ; }#[cfg(target_os = "windows" )] #[link(name = "kernel32" )] extern "stdcall" { fn GetStdHandle (nStdHandle: i32 ) -> i32 ; fn WriteConsoleW (h: i32 , buf: *const u16 , ...); }println! ("Hello world from Stdlib" );
要点:
syscall 指令比早期 int 0x80 软中断快,借助
VDSO (附属于进程的内存页)避免上下文切换
Windows 的 WriteConsoleW 需要
utf-16 ,故需 message.encode_utf16()
转换;W=Unicode,A=ANSI
FFI 调用必须 unsafe;Linux/macOS API 简单相似,Windows
需要更多数据结构与代码
3.3 跨平台抽象的隐藏复杂性
代码只跑 Linux/macOS
很省事,一旦跨平台代码量爆炸。社区方案:libc
crate(封装函数+常量)、mio、libuv。边缘情况(如"所有合法
utf-8 都能转 utf-16 正确显示吗?")是额外的难题。
3.4 CPU 与 OS
的暗中合作(安全机制)
一个"为什么 CPU 知道不允许解引用
99999999999999"的实验引出整套硬件级安全机制:
graph TD
A["代码解引用指针"] --> B["MMU 查页表<br/>虚拟地址 → 物理地址"]
B -->|"找到映射"| C["正常读取数据 ✔"]
B -->|"页表中无映射"| D["触发 Page Fault 异常"]
D --> E["CPU 查中断描述符表 IDT<br/>(OS 启动时注册)"]
E --> F["跳转到 OS 的处理程序<br/>输出: segmentation fault"]
G["Ring 0 内核态"] -.->|"可改页表、访问设备"| B
H["Ring 3 用户态"] -.->|"受限访问, 违规即异常"| B
虚拟内存 + 页表 :每个进程有独立页表,CPU
特殊寄存器指向它
特权级 Ring 0(内核)/ Ring
3(用户) :用户态代码试图改页表 → 异常 → 跳转到 OS
处理程序。这就是除系统调用外你别无选择 与内核/硬件交互的原因
第 4 章:中断 | 固件 | I/O
4.1
从网卡读取数据的全流程(简化)
graph TD
A["1. 我们的代码<br/>syscall 注册 socket<br/>(得 fd / handle)"] --> B["2. 向 OS 注册<br/>感兴趣的事件"]
B --> C["3. 网卡固件<br/>专用微控制器检测到<br/>数据到达"]
C --> D["4. 硬件中断<br/>IRQ 电信号发给 CPU"]
D --> E["5. CPU 查 IDT<br/>跳到中断处理程序"]
E --> F["6. DMA<br/>网卡直接把数据<br/>写入内存缓冲区"]
F --> G["7. 驱动程序<br/>通知内核数据就绪"]
G --> H["8. OS 按注册方式通知我们"]
4.2 第 2 步"注册事件"的三种姿势
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 blocking_read (fd);match nonblocking_poll (fd) { Ready => read (fd), NotReady => } event_queue.register (fd, Read);for event in event_queue.wait () { read (event.fd); }
4.3 硬件中断 / 软件中断 / 固件
硬件中断 :IRQ 线上的电信号,随时打断 CPU →
存寄存器状态 → 查 IDT 跳转处理程序
软件中断 :由指令主动触发(如旧式
int 0x80 系统调用)
IDT 位于主内存 的固定位置,CPU
只在寄存器存指向它的指针(网卡类中断的处理程序通常由驱动程序 注册)
固件 :系统中"隐藏的小 CPU"无处不在(网卡、甚至 CPU
自身)。网卡固件已在轮询数据,我们若再让 CPU
忙轮询就是重复劳动——并发即效率 的又一体现
第 5 章:处理 I/O 的三大策略
策略对比总表
代表
传统服务器
Go
Node / Rust async
优点
简单易写;性能尚可;免费获得并行
用法像 OS 线程;可控调度/优先级
接近最佳资源利用率 ;最大灵活性
缺点
栈大(海量任务耗尽内存);syscall 开销大;OS 调度不可控
需要运行时,重复 OS 已做的事;实现难
各 OS
差异大;复杂;只解决"等待",仍需暂停任务的方案
for conn in incoming { thread::spawn (move || handle (conn)); } go!(|| handle (conn)); epoll.register (conn, READABLE, token);loop { for ev in epoll.wait () { dispatch (ev); } }
关键结论 :策略 3
只解决了"何时知道就绪",如何暂停/恢复任务 仍需上层方案——
Node 的答案:回调 (callbacks)
Rust 的答案:Futures 状态机(暂停点即状态)
Node 运行时 = 策略 1 + 策略 3 组合,但尽量强制 I/O 走策略 3——这是
Node 擅长海量并发连接的原因。
第 6 章:Epoll | Kqueue | IOCP
6.1 谁在用它们
Node → libuv (跨平台异步 I/O 库, Julia/Pyuv 也用) Rust → mio (tokio 的 OS 事件队列后端; tokio之于Actix Web...) 本书 → minimio (作者手写的玩具版 mio)
6.2 两种模型的根本差异
sequenceDiagram
participant P as epoll/kqueue (就绪型)
Note over P: 告诉你"可以读了", 数据你自己取
P->>P: epoll_create()/kqueue() 建队列
P->>P: 注册 fd 的 Read 兴趣
P->>P: epoll_wait()/kevent() 阻塞等待
P-->>P: 返回"socket N 可读"
P->>P: 自己调 read(N)
participant C as IOCP (完成型)
Note over C: 告诉你"已经读好了", 数据已在缓冲区
C->>C: CreateIoCompletionPort() 建队列
C->>C: 注册 Read + 借出缓冲区给 OS
C->>C: GetQueuedCompletionStatusEx() 阻塞
C-->>C: 返回"数据已读入你的缓冲区"
模型
就绪型 readiness
就绪型 readiness
完成型 completion
通知含义
"可以执行操作了"
同左
"操作已完成,数据已进缓冲区"
缓冲区
自己管理
自己管理
借给 OS (等待期间不可动,借用检查器大显身手)
特点
大量事件下高效
概念类似但更抽象通用
官方文档好
跨平台库设计经验 :让"就绪型"表现得像"完成型"更容易 →
先按 IOCP(完成型)设计,再适配 epoll/kqueue 。
第 7 章:例子
7.1 目标代码(JS 风格的 Rust)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 fn javascript () { Fs::read ("test.txt" , |result| { let text = result.into_string ().unwrap (); Crypto::encrypt (text.len (), |result| { print (result.into_int ().unwrap ()); }); }); set_timeout (0 , |_| print ("Immediate1" )); Http::http_get_slow ("www.google.com" , 2000 , |result| { print_content (result.into_string ().unwrap (), "web call" ); }); }fn main () { let rt = Runtime::new (); rt.run (javascript); }
Js 枚举模拟 JS
的动态类型(Undefined/String/Int),因为 Rust
是静态类型语言。
7.2 什么是 Node(破除误解)
graph TD
subgraph Node 架构
V["V8 引擎<br/>JIT 编译执行 JS<br/>(不能 I/O)"]
R["Node 运行时"]
EL["事件循环<br/>(用户代码只跑在这一个线程!)"]
TP["线程池 x4<br/>(默认)"]
EQ["libuv<br/>epoll/kqueue/IOCP 事件队列"]
end
JS["你的 JS 代码"] --> V --> R
R --> EL
EL -->|"I/O 密集"| EQ
EL -->|"CPU 密集 / epoll 处理不了的 I/O<br/>(如文件读取)"| TP
JS 本身没有事件循环 ——浏览器或 Node
提供运行时才有
Node 是多线程的 (线程池 + epoll
线程),但你的代码只跑在单个主线程 上——"别阻塞事件循环"指的就是它
受 I/O 限制 → 事件队列;受 CPU 限制 → 线程池(大多数 C++
扩展也在此执行)
7.3 实现计划
需要:两个事件队列 (线程池 +
minimio)|一个运行时 (存回调、发任务、注册事件、轮询、处理计时器)|三个模块 (Fs/Crypto/Http)|辅助函数(print/print_content/current)
第 8 章:实现自己的运行时
8.1 运行时的状态(Runtime
struct 逐字段)
pub struct Runtime { available_threads: Vec <usize >, callbacks_to_run: Vec <(usize , Js)>, callback_queue: HashMap<usize , Box <dyn FnOnce (Js)>>, epoll_pending_events: usize , epoll_registrator: minimio::Registrator, epoll_thread: thread::JoinHandle<()>, epoll_timeout: Arc<Mutex<Option <i32 >>>, event_reciever: Receiver<PollEvent>, identity_token: usize , pending_events: usize , thread_pool: Vec <NodeThread>, timers: BTreeMap<Instant, usize >, timers_to_remove: Vec <Instant>, }
支撑类型:
struct Task { task: Box <dyn Fn () -> Js + Send >, callback_id: usize , kind: ThreadPoolTaskKind, }enum PollEvent { Threadpool ((thread_id, callback_id, Js)), Epoll (event_id), Timeout, }static mut RUNTIME: *mut Runtime = std::ptr::null_mut ();
8.2 主循环(仿 Node 的 6 阶段)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 pub fn run (mut self , f: impl Fn ()) { unsafe { RUNTIME = &mut self }; f (); while self .pending_events > 0 { self .process_expired_timers (); self .run_callbacks (); let next_timeout = self .get_next_timer (); *self .epoll_timeout.lock ().unwrap () = next_timeout; drop (lock); if let Ok (event) = self .event_reciever.recv () { match event { Timeout => (), Threadpool ((tid, cb_id, data)) => self .process_threadpool_events (..), Epoll (event_id) => self .process_epoll_events (..), } } self .run_callbacks (); } }
graph TD
S["run(f) 先同步执行用户代码<br/>注册 timers/线程池任务/epoll 事件"] --> L{"pending_events > 0 ?"}
L -->|"否"| Z["清理并退出"]
L -->|"是"| T1["1. Timers<br/>到期回调入队"]
T1 --> T2["2. Callbacks<br/>执行排队的回调"]
T2 --> T4["4. Poll<br/>设 timeout=最近计时器<br/>阻塞 recv 事件通道"]
T4 --> E{"收到什么事件?"}
E -->|"Timeout"| T1
E -->|"Threadpool 完成"| T2
E -->|"Epoll 就绪"| T2
8.3 线程池的搭建(Runtime::new
上半场)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let (event_sender, event_receiver) = channel::<PollEvent>();for i in 0 ..4 { let (evt_sender, evt_receiver) = channel::<Task>(); let event_sender = event_sender.clone (); let handle = thread::Builder::new ().name (format! ("pool{}" , i)) .spawn (move || { while let Ok (task) = evt_receiver.recv () { if let ThreadPoolTaskKind ::Close = task.kind { break ; } let res = (task.task)(); event_sender.send (PollEvent::Threadpool ((i, task.callback_id, res))); } }).unwrap (); threads.push (NodeThread { handle, sender: evt_sender }); }
8.4 epoll 线程(Runtime::new
下半场)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 let mut poll = minimio::Poll::new ().unwrap (); let registrator = poll.registrator (); let epoll_timeout = Arc::new (Mutex::new (None )); thread::Builder::new ().name ("epoll" .into ()).spawn (move || { let mut events = minimio::Events::with_capacity (1024 ); loop { let timeout = *epoll_timeout_clone.lock ().unwrap (); drop (lock); match poll.poll (&mut events, timeout) { Ok (v) if v > 0 => { } Ok (0 ) => { } Err (Interrupted) => break , Err (e) => panic! (e), } } });
两个 drop(lock)
是全书最重要的并发细节 :poll/recv
会阻塞线程,若持有锁去等待,另一端将永远无法写入 →
死锁 。
8.5 Timers:BTreeMap 的妙用
fn process_expired_timers (&mut self ) { self .timers.range (..=Instant::now ()) .for_each (|(k, _)| timers_to_remove.push (*k)); while let Some (key) = self .timers_to_remove.pop () { let cb_id = self .timers.remove (&key).unwrap (); self .callbacks_to_run.push ((cb_id, Js::Undefined)); } }fn get_next_timer (&self ) -> Option <i32 > { self .timers.iter ().nth (0 ).map (|(&instant, _)| (instant - Instant::now ()).as_millis () as i32 ) }
选 BTreeMap 而非
HashMap/BST:键天然有序;相比 BST
每节点存多个值(小 Vec),缓存友好 。
8.6
Callbacks:Box<dyn FnOnce(Js)>
fn run_callbacks (&mut self ) { while let Some ((cb_id, data)) = self .callbacks_to_run.pop () { let cb = self .callback_queue.remove (&cb_id).unwrap (); cb (data); self .pending_events -= 1 ; } }
FnOnce:闭包捕获并消耗 环境变量,只能调用一次——正是回调语义,还顺便借
RAII 清理资源。trait 无固定大小 → 必须 Box 上堆。
8.7 三个注册
API(模块与运行时的接口)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 fn register_event_epoll (&mut self , token: usize , cb: impl FnOnce (Js)) { self .add_callback (token, cb); self .pending_events += 1 ; }fn register_event_threadpool (&mut self , task: impl Fn ()-> Js+Send , kind: ThreadPoolTaskKind, cb: impl FnOnce (Js)) { let cb_id = self .generate_cb_identity (); self .add_callback (cb_id, cb); let available = self .get_available_thread (); self .thread_pool[available].sender.send (Task { task: Box ::new (task), .. }); self .pending_events += 1 ; }fn set_timeout (&mut self , ms: u64 , cb: impl Fn (Js)) { let cb_id = self .generate_cb_identity (); self .add_callback (cb_id, cb); self .timers.insert (Instant::now () + Duration::from_millis (ms), cb_id); self .pending_events += 1 ; }
事件回流的处理:
fn process_threadpool_events (&mut self , thread_id, callback_id, data: Js) { self .callbacks_to_run.push ((callback_id, data)); self .available_threads.push (thread_id); }fn process_epoll_events (&mut self , event_id: usize ) { self .callbacks_to_run.push ((event_id, Js::Undefined)); }
安全扩展 :完成型模型(IOCP)要为每个 Read/Write
借出缓冲区,恶意客户端可借注册大量不发生的事件耗尽内存——生产实现需设"高水位线"限制待处理事件数。
8.8 基础设施杂项
fn generate_identity (&mut self ) -> usize { self .identity_token = self .identity_token.wrapping_add (1 ); self .identity_token }
第 9 章:模块(相当于 Node 的
C++ 扩展)
9.0 全局 RUNTIME 的 unsafe
及为何安全
pub fn set_timeout (ms: u64 , cb: impl Fn (Js) + 'static ) { let rt = unsafe { &mut *(RUNTIME as *mut Runtime) }; rt.set_timeout (ms, cb); }
9.1 Fs 模块 +
彩蛋:为什么文件 I/O 走线程池而不是 epoll?
impl Fs { fn read (path: &'static str , cb: impl Fn (Js)) { let work = move || { thread::sleep (Duration::from_secs (1 )); let mut buffer = String ::new (); fs::File::open (&path).unwrap ().read_to_string (&mut buffer).unwrap (); Js::String (buffer) }; unsafe { &mut *RUNTIME }.register_event_threadpool (work, FileRead, cb); } }
文件 I/O 走线程池的原因:
OS
大量缓存文件,读通常立即就绪 ——注册事件+等通知反而比直接读更慢
但"从 OS 缓存拷进你的缓冲区"仍耗时,会阻塞主循环 → 丢给线程池
Linux/macOS 的就绪型模型对异步文件支持差;Windows(IOCP)/新版
Linux(io_uring) 才有好的完成型支持
线程池方案: 代码简单 ✔ | 性能够好 ✔ | 多数场景(如 web 服务器)几乎无损失 异步文件I/O: 复杂度高 ✘ | API 贫乏且平台差异大 ✘ | 实际收益甚微
9.2 Crypto 模块(CPU
密集型代表)
impl Crypto { fn encrypt (n: usize , cb: impl Fn (Js)) { let work = move || { fn fib (n: usize ) -> usize { match n { 0 => 0 , 1 => 1 , _ => fib (n-1 ) + fib (n-2 ) } } Js::Int (fib (n)) }; unsafe { &mut *RUNTIME }.register_event_threadpool (work, Encrypt, cb); } }
9.3 Http 模块(epoll
路线的代表)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 impl Http { fn http_get_slow (url: &str , delay_ms: u32 , cb: impl Fn (Js)) { let rt = unsafe { &mut *RUNTIME }; let mut stream = minimio::TcpStream::connect ("slowwly...:80" ).unwrap (); stream.write_all (format! ("GET /delay/{} ... HTTP/1.1\r\n..." , delay_ms).as_bytes ()); let token = rt.generate_cb_identity (); rt.epoll_registrator.register (&mut stream, token, minimio::Interests::READABLE).unwrap (); let wrapped = move |_| { let mut buffer = String ::new (); stream.read_to_string (&mut buffer); cb (Js::String (buffer)); }; rt.register_event_epoll (token, wrapped); } }
伪唤醒 (spurious wakeup) :OS
可能"不确定事件是否发生"时依然唤醒线程。契约要求程序员重注册事件再检查 ,而非假设数据已到。真实实现应在读到
WouldBlock 时重新注册读事件。
第 10~11 章:组装与最终代码
10.1 入口与全局架构图
fn main () { let rt = Runtime::new (); rt.run (javascript); }
graph TD
subgraph 主线程 main
M["Runtime::run 事件循环<br/>timers→callbacks→poll→..."]
CB["callback_queue / callbacks_to_run"]
T["timers (BTreeMap)"]
end
subgraph 工作者
P0["pool0"]
P1["pool1~3"]
E["epoll 线程<br/>minimio::Poll"]
end
OS["OS 事件队列<br/>epoll/kqueue/IOCP"]
R["Registrator<br/>(主线程持有)"]
M -->|"Task 经每线程独立通道"| P0
M -->|"Task"| P1
P0 -->|"PollEvent::Threadpool(id,cb_id,Js)"| CH["event_reciever<br/>(共享 mpsc 通道)"]
P1 --> CH
E -->|"PollEvent::Epoll(token)"| CH
E -->|"PollEvent::Timeout"| CH
CH --> M
R -->|"register(fd, token, READABLE)"| OS
OS -->|"就绪事件+token"| E
M -.->|"写入共享 epoll_timeout"| E
11.1 运行输出揭示了什么(TICK
视角)
main: ===== TICK 1 ===== main: Immediate1 timed out / Immediate2 timed out ← 0ms 计时器第一轮就到期 pool2/pool3: finished File read ← 线程池与主循环并行工作 main: First count: 39 characters. ← 文件结果回调 main: ===== TICK 2 ===== main: SETTIMEOUT ← 1000ms 计时器 ... epoll: epoll event 7 is ready ← http 响应就绪 ... main: FINISHED
关键观察:
用户代码注册的所有 事件先同步注册完,循环才开始
回调在主线程跑,任务在线程池/OS
跑 ——这就是"并发但不(在用户代码层面)并行"
嵌套回调(文件读完再读下一个)自然产生新的 TICK
pending_events 每注册 +1、每回调执行
-1,归零即退出
第 12 章:捷径与改进
书中的简化点:
fn run_callbacks (&mut self ) { let mut n = 0 ; while n < MAX_CALLBACKS_PER_TICK { n += 1 ; } }fn get_available_thread (&mut self ) -> Option <usize > { self .available_threads.pop () }
全书核心结论
同步是错觉 :从程序员/OS/CPU
三个参照系看,"顺序执行"只在第一个成立
并发管效率,并行管吞吐 :并发不能让单个任务更快,只能让一组任务总耗时更优
一切 I/O 皆系统调用 :用户态(Ring 3)到内核态(Ring
0)没有捷径
硬件早已为异步准备好 :中断(IDT/IRQ) + DMA +
固件,CPU 全程参与事件通知
策略 3(OS 事件队列)
是最优解但只解决一半 ——"如何暂停任务"由运行时决定:Node
用回调 ,Rust 用状态机 Future
事件循环 = Timers → Callbacks → Poll(阻塞等待三路事件)
的循环 ,pending_events == 0 时自然退出
"别阻塞事件循环"的机理 :所有回调都在主线程串行执行,一个慢回调
= 所有任务停摆
锁与阻塞的交互是死锁高发区 :先 drop
锁再去 poll/recv
就绪型(epoll/kqueue)与完成型(IOCP)的抽象差异 是跨平台异步库的核心难题(借出缓冲区的所有权问题甚至惊动了借用检查器)
延伸阅读:Exploring Epoll, Kqueue and IOCP with
Rust 、Green threads explained in 200 lines of
Rust 、Exploring Rust Futures