综合项目:类型安全的任务调度器
本项目将全书的模式整合到一个单一的、生产级别的系统中。你将构建一个类型安全的并发任务调度器,它使用泛型、traits、typestate、通道、错误处理和测试。
预计时间:4-6 小时 | 难度:★★★
你将练习:
- 泛型和 trait 边界(第 1-2 章)
- 任务生命周期的 typestate 模式(第 3 章)
- 零成本状态标记的 PhantomData(第 4 章)
- worker 通信的通道(第 5 章)
- 使用作用域线程的并发(第 6 章)
- 使用
thiserror的错误处理(第 9 章)- 使用属性基测试进行测试(第 13 章)
- 使用
TryFrom和验证类型的 API 设计(第 14 章)
问题
构建一个任务调度器,其中:
- 任务具有类型化的生命周期:
Pending → Running → Completed(或Failed) - Workers 从通道获取任务,执行它们,并报告结果
- 调度器 管理任务提交、worker 协调和结果收集
- 无效的状态转换是编译时错误
stateDiagram-v2
[*] --> Pending: scheduler.submit(task)
Pending --> Running: worker picks up task
Running --> Completed: task succeeds
Running --> Failed: task returns Err
Completed --> [*]: scheduler.results()
Failed --> [*]: scheduler.results()
Pending --> Pending: ❌ can't execute directly
Completed --> Running: ❌ can't re-run
步骤 1:定义任务类型
从 typestate 标记和通用 Task 开始:
#![allow(unused)]
fn main() {
use std::marker::PhantomData;
// --- 状态标记(零大小)---
struct Pending;
struct Running;
struct Completed;
struct Failed;
// --- 任务 ID(用于类型安全的 newtype)---
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct TaskId(u64);
// --- Task 结构体,按生命周期状态参数化 ---
struct Task<State, R> {
id: TaskId,
name: String,
_state: PhantomData<State>,
_result: PhantomData<R>,
}
}
你的工作:实现状态转换,使得:
Task<Pending, R>可以转换为Task<Running, R>(通过start())Task<Running, R>可以转换为Task<Completed, R>或Task<Failed, R>- 没有其他转换可以编译
💡 提示
每个转换方法应该消费 self 并返回新状态:
#![allow(unused)]
fn main() {
impl<R> Task<Pending, R> {
fn start(self) -> Task<Running, R> {
Task {
id: self.id,
name: self.name,
_state: PhantomData,
_result: PhantomData,
}
}
}
}
步骤 2:定义工作函数
任务需要执行的函数。使用 boxed 闭包:
#![allow(unused)]
fn main() {
struct WorkItem<R: Send + 'static> {
id: TaskId,
name: String,
work: Box<dyn FnOnce() -> Result<R, String> + Send>,
}
}
你的工作:实现 WorkItem::new() 接受任务名称和闭包。
添加 TaskId 生成器(简单的原子计数器或 mutex 保护的计数器)。
步骤 3:错误处理
使用 thiserror 定义调度器的错误类型:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SchedulerError {
#[error("scheduler is shut down")]
ShutDown,
#[error("task {0:?} failed: {1}")]
TaskFailed(TaskId, String),
#[error("channel send error")]
ChannelError(#[from] std::sync::mpsc::SendError<()>),
#[error("worker panicked")]
WorkerPanic,
}
步骤 4:调度器
使用通道(第 5 章)和作用域线程(第 6 章)构建调度器:
#![allow(unused)]
fn main() {
use std::sync::mpsc;
struct Scheduler<R: Send + 'static> {
sender: Option<mpsc::Sender<WorkItem<R>>>,
results: mpsc::Receiver<TaskResult<R>>,
num_workers: usize,
}
struct TaskResult<R> {
id: TaskId,
name: String,
outcome: Result<R, String>,
}
}
你的工作:实现:
Scheduler::new(num_workers: usize) -> Self—— 创建通道并生成 workersScheduler::submit(&self, item: WorkItem<R>) -> Result<TaskId, SchedulerError>Scheduler::shutdown(self) -> Vec<TaskResult<R>>—— drop sender,加入 workers,收集结果
💡 提示 —— Worker 循环
#![allow(unused)]
fn main() {
fn worker_loop<R: Send + 'static>(
rx: std::sync::Arc<std::sync::Mutex<mpsc::Receiver<WorkItem<R>>>>,
result_tx: mpsc::Sender<TaskResult<R>>,
worker_id: usize,
) {
loop {
let item = {
let rx = rx.lock().unwrap();
rx.recv()
};
match item {
Ok(work_item) => {
let outcome = (work_item.work)();
let _ = result_tx.send(TaskResult {
id: work_item.id,
name: work_item.name,
outcome,
});
}
Err(_) => break, // 通道关闭
}
}
}
}
步骤 5:集成测试
编写测试验证:
- Happy path:提交 10 个任务,关闭,验证所有 10 个结果都是
Ok - 错误处理:提交失败的任务,验证
TaskResult.outcome是Err - 空调度器:创建并立即关闭 —— 无 panic
- 属性测试(bonus):使用
proptest验证对于任意 N 个任务(1..100),调度器总是返回恰好 N 个结果
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn happy_path() {
let scheduler = Scheduler::<String>::new(4);
for i in 0..10 {
let item = WorkItem::new(
format!("task-{i}"),
move || Ok(format!("result-{i}")),
);
scheduler.submit(item).unwrap();
}
let results = scheduler.shutdown();
assert_eq!(results.len(), 10);
for r in &results {
assert!(r.outcome.is_ok());
}
}
#[test]
fn handles_failures() {
let scheduler = Scheduler::<String>::new(2);
scheduler.submit(WorkItem::new("good", || Ok("ok".into()))).unwrap();
scheduler.submit(WorkItem::new("bad", || Err("boom".into()))).unwrap();
let results = scheduler.shutdown();
assert_eq!(results.len(), 2);
let failures: Vec<_> = results.iter()
.filter(|r| r.outcome.is_err())
.collect();
assert_eq!(failures.len(), 1);
}
}
}
步骤 6:整合
这是演示完整系统的 main():
fn main() {
let scheduler = Scheduler::<String>::new(4);
// 提交具有不同工作负载的任务
for i in 0..20 {
let item = WorkItem::new(
format!("compute-{i}"),
move || {
// 模拟工作
std::thread::sleep(std::time::Duration::from_millis(10));
if i % 7 == 0 {
Err(format!("task {i} hit a simulated error"))
} else {
Ok(format!("task {i} completed with value {}", i * i))
}
},
);
// 注意:.unwrap() 用于简洁 —— 在生产环境中处理 SendError。
scheduler.submit(item).unwrap();
}
println!("All tasks submitted. Shutting down...");
let results = scheduler.shutdown();
let (ok, err): (Vec<_>, Vec<_>) = results.iter()
.partition(|r| r.outcome.is_ok());
println!("\n✅ Succeeded: {}", ok.len());
for r in &ok {
println!(" {} → {}", r.name, r.outcome.as_ref().unwrap());
}
println!("\n❌ Failed: {}", err.len());
for r in &err {
println!(" {} → {}", r.name, r.outcome.as_ref().unwrap_err());
}
}
评估标准
| 标准 | 目标 |
|---|---|
| 类型安全 | 无效状态转换无法编译 |
| 并发 | Workers 并行运行,无数据竞争 |
| 错误处理 | 所有失败都捕获在 TaskResult 中,无 panic |
| 测试 | 至少 3 个测试;bonus 给 proptest |
| 代码组织 | 清晰的模块结构,公共 API 使用验证类型 |
| 文档 | 关键类型有文档注释解释不变量 |
扩展想法
一旦基本调度器工作,尝试这些增强:
- 优先级队列:添加
Prioritynewtype(1-10)并优先处理更高优先级的任务 - 重试策略:失败的任务重试最多 N 次,然后标记为永久失败
- 取消:添加
cancel(TaskId)方法移除待处理任务 - Async 版本:使用
tokio::sync::mpsc通道移植到tokio::spawn(第 15 章) - 指标:跟踪每个 worker 的任务计数、平均执行时间和失败率