外观
第 7 节:优雅停机——让线程好好说再见
症状
服务器(第 20 章)按 Ctrl+C 想关机,正在干活的请求被硬生生掐断——用户看到的页面是一半的;或者反过来,程序关了线程还在后台跑,数据写了一半。
诊断
硬停机:main 结束/进程被杀死,线程直接被"拖走"——正在写的文件、正在处理的请求,全部半途而废。真实服务器要的是优雅停机(graceful shutdown):告诉工作线程"别再接新活了",等它们干完手头的活,再关灯下班。
怎么告诉线程"下班"?标准答案:channel 发信号(第 15 章接力棒的老朋友)。
解药:channel 当"下班铃" ⭐
rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (sender, receiver) = mpsc::channel();
// 工作线程:循环干活,时不时看一眼"下班铃"
let worker = thread::spawn(move || {
loop {
match receiver.try_recv() {
Ok(message) => {
println!("收到下班信号:{}", message);
println!("干完手头的活,优雅下班");
break;
}
Err(mpsc::TryRecvError::Empty) => {
// 没信号:继续干活
println!("干活中……");
thread::sleep(Duration::from_millis(300));
}
Err(mpsc::TryRecvError::Disconnected) => {
println!("老板走了,收拾东西下班"); // 发送端全关了
break;
}
}
}
});
// 主线程干完自己的事,喊下班
thread::sleep(Duration::from_millis(1000));
println!("老板:今天辛苦了,下班!");
sender.send("下班").expect("发送失败");
worker.join().expect("worker 崩溃了"); // 等它真的干完
println!("程序正常退出");
}运行输出
text
干活中……
干活中……
干活中……
干活中……
老板:今天辛苦了,下班!
收到下班信号:下班
干完手头的活,优雅下班
程序正常退出三个关键动作:
try_recv()不阻塞地看信号:没信号 →Empty,继续干活;有信号 →Ok,收尾下班sender.send("下班"):主线程摇下班铃worker.join():主线程等 worker 真的干完才退出——"优雅"的关键一步
进阶:Drop 自动喊下班 ⭐
把"喊下班 + 等收尾"包进结构体的 Drop(第 14 章),结构体一丢,自动优雅停机:
rust
struct Worker {
sender: mpsc::Sender<String>,
handle: Option<thread::JoinHandle<()>>,
}
impl Drop for Worker {
fn drop(&mut self) {
let _ = self.sender.send("下班".to_string()); // 摇铃
if let Some(handle) = self.handle.take() {
handle.join().expect("worker 崩溃了"); // 等人
}
}
}真实服务器(TRPL 第 20 章的线程池)就是这么做的:Worker 被 drop → 发停机信号 → 等所有请求处理完 → 才真正退出。
预防
- 优雅停机的三步曲:停接新活 → 等手头活干完 → 才退出——缺任何一步都是"假优雅"
join()是"等人",recv()是"等消息"(第 15 章):停机信号用 channel,等人用 join,两个都等才稳- 线程的三种"走"法:自然退出(函数结束)、信号退出(try_recv 到下班铃)、被拖走(程序强制结束)——真实服务器只用前两种
- 异步版:tokio 里有
CancellationToken(tokio-util)做同样的停机信号;原理一样:一个标志,任务们检查后收尾