跳转至

终章项目:多线程 Web 服务器

这一章做什么: 全书毕业设计——先手写一个单线程 Web 服务器(听懂 TCP/HTTP 的皮毛), 再造一个 ThreadPool,让服务器用固定数量的线程并发处理请求,最后补上优雅停机。 读完你要能复述线程池的完整设计:任务怎么进队、怎么被领走、怎么停。 书在开头说了两条前提:这不会是"最好的写法"(crates.io 上有生产级现成货), 也不用 async——"我们手写,是为了让你看懂将来要用的那些 crate 底下发生了什么"1。

1. 顶层全景:五步计划与一次资源升级

  ① TCP 监听 + HTTP 解析(请求行 GET / HTTP/1.1 + headers + body)
  ② 回固定 HTML(Hello!)
  ③ 加 /sleep 慢请求 → 暴露问题:单线程时第二个请求干等 5 秒
  ④ ThreadPool:固定 4 个 Worker 线程,任务进通道队列,谁空谁领
  ⑤ 优雅停机:drop 发送端 → Worker 收 Err → 退出循环 → join 收尾

  图说:③ 的卡顿是动机;④ 用"有限的线程"换"请求不互相堵";
  ⑤ 回答"进程怎么体面地死"。

单线程版的两件事:TCP 是字节流协议,HTTP 是文本协议;TcpListener::bind("127.0.0.1:7878") 监听,listener.incoming() 逐个收 TcpStream,按行读到空行拿到请求(请求行形如 GET / HTTP/1.1——其中 / 叫 URI,统一资源标识符,就是你要的资源的路径),回一段手写的 HTTP 响应(状态行 + Content-Length + body)。/sleep 故意 thread::sleep(5 秒)——模拟慢请求,暴露单线程的致命伤:一个连接占着线程,后面全排队2。

2. ThreadPool:四个部件的组装

公开 API 只有两行:ThreadPool::new(4) 建池;pool.execute(闭包) 派活。内部装配(每一环都撞过一次编译器,书里逐一展开):

部件一:通道当任务队列。 ThreadPool 持有 sender;Job 定义为类型别名 type Job = Box<dyn FnOnce() + Send + 'static>——execute 的泛型参数写成同样的 bound:任务是一次性的(FnOnce)、能跨线程(Send)、不借环境('static)3。

部件二:共享接收端 = Arc<Mutex>。 第一个编译错误:receiver 想发给所有 Worker——E0382,且 mpsc 语义是"多生产者单消费者",接收端 clone 不出来。解法正是第 16 章的组合:Arc::new(Mutex::new(receiver)),每个 Worker 拿一份 Arc::clone。Mutex 保证同一时刻只有一个 Worker 在取任务4。

部件三:Worker 循环。 每个 Worker 是"id + 一个永远在跑的线程",线程体内:

loop {
    let job = receiver.lock().unwrap().recv().unwrap();
    println!("Worker {id} got a job; executing.");
    job();
}

recv 阻塞——没活时线程睡觉,不占 CPU。到这里线程池已经能跑:4 个 Worker 轮流领连接,/sleep 不再堵别人5。

部件四(真金①):锁的生命周期陷阱。 直觉上会想把它写成 while let Ok(job) = receiver.lock().unwrap().recv() { job(); }——能编译,但行为错了:慢请求又堵全池。原因微妙:Mutex 没有公开的 unlock 方法,放锁靠 MutexGuard 的 Drop;let 语句结束就 drop 临时值,而 while let 把临时值活到块尾。后者意味着锁在整个 job() 执行期间一直被持有——其他 Worker 干等。写法决定锁的持有期——这是把第 16 章"守卫自动放锁"理解到位后才能避开的坑6。

3. 优雅停机:三动作的配合

Ctrl-C 会半路掐断正在处理的请求。体面退出的链条:

  1. 给 ThreadPool 实现 Drop:for worker in self.workers.drain(..) { worker.thread.join() }。中途撞 E0507(join 要吃掉 JoinHandle,而手里只有 &mut)——解法:Vec::drain(..) 把 Worker 按值取出(书里顺带讨论了另一条路 Option::take,并说"明知永远有值就别包 Option,drain 更干净")7。
  2. 只 join 不够:Worker 在无限 loop 里等任务,join 会永远等。要先 drop(self.sender.take())——sender 字段包成 Option 就为了这一口 take。发送端一 drop,通道关闭,所有 Worker 的 recv 立刻返回 Err8。
  3. Worker 侧配合:recv 的结果改 match——Ok 干活,Err 打印一句"disconnected; shutting down"然后 break。线程循环退出,join 收工9。

验证方式很教学:.take(2) 只接两个请求就退出循环,看四个 Worker 按序打出停机消息。

4. 作者的判断与证据

  • "手写而非用现成 crate,是为了学通用思想;真实项目去 crates.io 找"——本章的自我定位,开篇即声明1。
  • "async 也能解这个问题,但我们先学线程池——许多 async 运行时的底下也是线程池"——第 17、21 章的呼应1。
  • while let 锁陷阱——本章最值钱的一处机制细节,书用了"subtle"一词并给出完整因果链6。

判断(我们的,不是书里的): 本章的真正课程不是"怎么写 Web 服务器", 而是"怎么在 Rust 里造一个并发原语"的完整方法论:每个设计决策都由一次编译错误逼出 (receiver 不能 clone → Arc;跨线程要 Send → bound 写进 Job;join 要吃所有权 → drain), 最后一处锁陷阱告诉你:机制对了,写法错了,依然错。这和第 12 章的"重构驱动"是同一种教学: 让编译器当助教。 如果错,会错在: 这个 ThreadPool 是教学品——没有任务超时、没有 Worker panic 恢复、 没有背压(无界通道);书也明说生产级线程池在 crates.io 上。把它当"思想示范"是对的, 当"可以抄的工业实现"是错的。

5. 边界与局限

  • HTTP 只解析到"能跑"的程度;TLS、keep-alive、chunked 全无。
  • 没有超时与错误恢复;unwrap 遍布(书末练习第一条就是换掉它们)。
  • 只演示 IPv4 本机回环。

6. 可带走的

  1. TCP 流 + 手写 HTTP 响应的最小骨架(listener / incoming / stream)。
  2. 线程池 = 通道(队列)+ Arc<Mutex>(共享接收端)+ 常驻 Worker 循环。
  3. type Job = Box<dyn FnOnce() + Send + 'static>:任务对象的完整签名。
  4. MutexGuard 的活期:let 语句尾 drop;while let/match 块尾才 drop——锁陷阱。
  5. 优雅停机 = drop(sender) → recv 出 Err → break → join。
  6. Vec::drain(..) 按值取空向量,免 Option 包裹。
  7. 学完手写版,去 crates.io 用真线程池/真 HTTP 库。

7. 原文地图

主题 原书章 原文位置
项目定位(手写 vs crate / 不用 async) Chapter 21: Final Project text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:21(搜「won’t be the best way」)
单线程服务器与 /sleep 问题 同上 text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:39(搜「Single-Threaded Web Server」)
计划:通道当队列 同上 text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1061(搜「function as the queue」)
receiver 共享:Arc<Mutex> 同上 text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1154(搜「use of moved value」) · text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1176(搜「thread-safe smart pointers」)
Job 类型别名与 execute 同上 text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1237(搜「type Job」)
Worker 循环与锁陷阱 同上 text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1278(搜「let job = receiver」) · text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1403(搜「remains held」)
Drop 与 drain 同上 text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1448(搜「cannot move out」) · text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1478(搜「drain method」)
停机信号链 同上 text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1546(搜「drop(self.sender.take())」) · text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1577(搜「disconnected」)


  1. 出处:「Chapter 21: Final Project」第 21 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:21,搜「won’t be the best way」)与第 17 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:17,搜「thread pool」)。 ↩↩↩

  2. 出处:「Chapter 21: Final Project」第 39 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:39,搜「Single-Threaded Web Server」)。 ↩

  3. 出处:「Chapter 21: Final Project」第 1237 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1237,搜「type Job」)。 ↩

  4. 出处:「Chapter 21: Final Project」第 1176 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1176,搜「thread-safe smart pointers」)。 ↩

  5. 出处:「Chapter 21: Final Project」第 1306 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1306,搜「recv blocks」)。 ↩

  6. 出处:「Chapter 21: Final Project」第 1403 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1403,搜「remains held」)。 ↩↩

  7. 出处:「Chapter 21: Final Project」第 1478 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1478,搜「drain method」)。 ↩

  8. 出处:「Chapter 21: Final Project」第 1546 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1546,搜「drop(self.sender.take())」)与第 1558 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1558,搜「closes the channel」)。 ↩

  9. 出处:「Chapter 21: Final Project」第 1577 段(text/28-ch21-chapter-21-final-project-building-a-multithreade.txt:1577,搜「disconnected」)。 ↩