ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Comprehensive Rust 并发同步实战:哲学家就餐问题与多线程链接检查器

Comprehensive Rust 并发同步实战:哲学家就餐问题与多线程链接检查器 Comprehensive Rust 并发同步实战哲学家就餐问题与多线程链接检查器【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust本文基于 Google Android 团队使用的 Rust 课程comprehensive-rust中「同步练习」Sync Exercises章节系统拆解两个经典并发实战练习经典的哲学家就餐问题Dining Philosophers与多线程链接检查器Multi-threaded Link Checker。读完本文你将掌握 Rust 中Mutex、Arc、mpsc通道的实际组合用法理解死锁产生的根源与打破对称性的解法并能够独立搭建一个基于线程池 通道的递归爬虫程序。练习总览本练习位于课程 并发章节 的收尾阶段紧接 共享状态Arc、Mutex与 通道mpsc两节的理论内容用于检验学习者能否把上述同步原语应用到真实场景中。整个练习由两个子练习构成练习核心知识点建议时长复杂度哲学家就餐问题ArcMutexT、mpsc::sync_channel、死锁避免20 分钟★★多线程链接检查器线程、通道、reqwest/scraper/thiserror生态20 分钟★★★仓库中对应的完整可运行源码位于 dining-philosophers.rs 与 link-checker.rs两份文件的完整解答都通过// ANCHOR:标记嵌入对应练习文档与 solutions.md 中是自检答案的权威参照。练习一哲学家就餐问题问题背景哲学家就餐问题是并发领域最经典的同步问题之一题目设定如下五位哲学家在同一张圆桌旁就餐。每位哲学家都有自己的座位每两个座位之间放着一根筷子。桌上的菜是意大利面需要两根筷子才能进食。每位哲学家只能交替地思考和吃饭并且只有同时拿到左右两根筷子才能吃面。因此只有当相邻的两位哲学家都在思考而非吃饭时这两根筷子才会同时空闲。某位哲学家吃完后会放下两根筷子。该问题与 Rust 的线程模型天然契合五位哲学家对应五个线程五根筷子对应五个共享资源。它恰好暴露了一个 Rust 并不会自动替你解决的问题——死锁。前置条件练习需要本地 Cargo 安装即本机已具备可用的 Rust 工具链。流程是把练习代码复制到本地src/main.rs中填补空白然后通过cargo run验证程序不会死锁。第一步准备 Cargo.toml练习建议使用如下最小化的Cargo.toml不含任何第三方依赖Mutex、Arc、mpsc全部来自标准库[package] name dining-philosophers version 0.1.0 edition 2024在仓库中该练习被组织为一个独立的 Cargo 工程其中通过[[bin]]声明了两个可执行文件目标dining-philosophers对应本练习[[bin]] name dining-philosophers path dining-philosophers.rs第二步搭建代码框架练习给出的代码框架去掉填空题后的结构如下struct Philosopher { name: String, // left_chopstick: ... // right_chopstick: ... // thoughts: ... } impl Philosopher { fn think(self) { // ... } fn eat(self) { // Pick up chopsticks... // ... } } fn main() { // Create chopsticks // Create philosophers // Make each of them think and eat 100 times // Output their thoughts }你需要完成的填空包括Philosopher结构体的字段类型左右筷子应该是ArcMutexChopstick思考通道应该是mpsc::SyncSenderString——这里的SyncSender是有界同步发送端与课程的通道章节内容呼应。eat方法中拿筷子的逻辑分别对左右筷子调用.lock().unwrap()。main中创建筷子、创建哲学家、循环 100 次思考与进食、输出想法。第三步跑通并消除死锁对照仓库中的参考解答 dining-philosophers.rs核心实现包含以下要点共享筷子五根筷子被Arc::new(Mutex::new(Chopstick))包裹哲学家用Arc::clone共享相邻的筷子let chopsticks PHILOSOPHERS .iter() .map(|_| Arc::new(Mutex::new(Chopstick))) .collect::Vec_();左右筷子分配第i位哲学家左手持第i根、右手持第(i 1) % 5根——环形分配使得每位哲学家的左右手资源在相邻哲学家中交错。打破对称、避免死锁的关键如果所有哲学家都「先拿左筷、再拿右筷」五人同时拿起左筷后就会互相等待对方手里的右筷形成死锁。参考解答对最后一位哲学家做了特殊处理——用std::mem::swap交换其左右筷子从而打破获取顺序的对称性// To avoid a deadlock, we have to break the symmetry // somewhere. This will swap the chopsticks without deinitializing // either of them. if i chopsticks.len() - 1 { std::mem::swap(mut left_chopstick, mut right_chopstick); }这里的std::mem::swap只交换两个Arc指针不会析构或移动底层Mutex注释特别强调它 will swap the chopsticks without deinitializing either of them。进食与思考eat依次锁住左右筷子、打印进食信息并thread::sleep(Duration::from_millis(10))模拟进食耗时think通过SyncSender::send把「Eureka! {name} has a new idea!」这类想法发回主线程fn eat(self) { println!({} is trying to eat, self.name); let _left self.left_chopstick.lock().unwrap(); let _right self.right_chopstick.lock().unwrap(); println!({} is eating..., self.name); thread::sleep(Duration::from_millis(10)); }主线程聚合输出主线程创建mpsc::sync_channel(10)每个哲学家线程持有一个克隆的tx主线程drop(tx)后for thought in rx会等所有哲学家线程结束发送端全部关闭后停止从而保证所有想法都被打印出来let (tx, rx) mpsc::sync_channel(10); // ... drop(tx); for thought in rx { println!({thought}); }执行规模每个哲学家线程循环 100 次「吃 → 想」哲学家名单固定为[Socrates, Hypatia, Plato, Aristotle, Pythagoras]。在仓库的 Bazel 构建中该练习同时被配置为可执行二进制与可运行测试目标见 BUILD.bazelrust_binary( name dining-philosophers, srcs [dining-philosophers.rs], deps all_crate_deps(normal True), ) rust_test( name dining-philosophers_test, size small, crate :dining-philosophers, )教学提示来自原文档课程原文档给出了两点教学建议鼓励学生先实现一个「大体能工作」的解决方案再逐步完善细节最简单解法中出现的死锁属于通用的并发问题恰好凸显了一个事实Rust 并不会自动阻止这类 bug。这是本练习最重要的教学价值——类型系统保证内存安全但不保证并发逻辑正确。练习二多线程链接检查器练习目标利用刚学到的并发知识实现一个多线程链接检查器。它从一个网页出发检查页面上的链接是否有效并递归地检查同一域名下的其他页面直到该域名下所有页面都被验证完毕。这是一个明显比哲学家就餐问题更大、更接近真实工程的项目课程也将其定位为「让学生在真实问题上卡壳、并与同学或讲师一起解决」的复杂练习。依赖选型练习需要三个 cratesreqwestHTTP 客户端负责下载页面scraperHTML 解析库用于提取页面中的链接thiserror过程宏驱动的错误类型派生库用于定义统一错误。创建项目与添加依赖cargo new link-checker cd link-checker cargo add --features blocking reqwest cargo add scraper cargo add thiserror提示如果cargo add报error: no such subcommand说明你的 Cargo 版本过旧请直接手工编辑Cargo.toml添加下方列出的依赖即可。上述cargo add执行后Cargo.toml应类似如下内容[package] name link-checker version 0.1.0 edition 2024 publish false [dependencies] reqwest { version 0.13.1, features [blocking] } scraper 0.25.0 thiserror 2.0.18注意三点reqwest开启了blockingfeature使用同步阻塞式客户端避免引入 async 复杂度让练习聚焦于线程与通道publish false表明这是一个本地练习项目不会发布到 crates.io仓库内实际使用的版本略有差异见 Cargo.tomlreqwest 0.13.4、scraper 0.27.0、thiserror 2.0.18并额外在dev-dependencies中声明了tempfile 3.27.0用于测试可作为版本兼容性的参考。建议先用一个小型站点测试例如https://www.google.org/。串行版本setup 与 visit_page练习给出的src/main.rs骨架由两部分组成setup错误类型与结构体use reqwest::Url; use reqwest::blocking::Client; use scraper::{Html, Selector}; use thiserror::Error; #[derive(Error, Debug)] enum Error { #[error(request error: {0})] ReqwestError(#[from] reqwest::Error), #[error(bad http response: {0})] BadResponse(String), }这里thiserror为错误枚举派生ErrortraitReqwestError通过#[from]自动实现Fromreqwest::Error从而支持?操作符的直接转换BadResponse携带 HTTP 状态码字符串通过#[error(bad http response: {0})]定义展示格式。visit_page单页抓取与链接提取#[derive(Debug)] struct CrawlCommand { url: Url, extract_links: bool, } fn visit_page(client: Client, command: CrawlCommand) - ResultVecUrl, Error { println!(Checking {:#}, command.url); let response client.get(command.url.clone()).send()?; if !response.status().is_success() { return Err(Error::BadResponse(response.status().to_string())); } let mut link_urls Vec::new(); if !command.extract_links { return Ok(link_urls); } let base_url response.url().clone(); let body_text response.text()?; let document Html::parse_document(body_text); let selector Selector::parse(a).unwrap(); let href_values document .select(selector) .filter_map(|element| element.value().attr(href)); for href in href_values { match base_url.join(href) { Ok(link_url) { link_urls.push(link_url); } Err(err) { println!(On {base_url:#}: ignored unparsable {href:?}: {err}); } } } Ok(link_urls) }visit_page的职责链清晰可读发送 GET 请求?自动把reqwest::Error转为Error::ReqwestError检查状态码非 2xx 返回Error::BadResponse用scraper的Html::parse_document解析 HTML选择器a选中所有a标签提取每个链接的href属性用base_url.join(href)把相对地址解析为绝对Url解析失败的链接被记录并忽略不会中断整体流程CrawlCommand.extract_links字段用于控制是否需要提取链接——这正是递归爬取时「只验证、不深挖外域链接」的关键开关。main串行演示fn main() { let client Client::new(); let start_url Url::parse(https://www.google.org).unwrap(); let crawl_command CrawlCommand{ url: start_url, extract_links: true }; match visit_page(client, crawl_command) { Ok(links) println!(Links: {links:#?}), Err(err) println!(Could not extract links: {err:#}), } }此时运行cargo run应当打印出首页上提取到的全部链接。这是整个练习的串行基座——先让它跑起来再引入线程。任务一用线程并行检查链接把待检查的 URL 发送到通道中让若干工作线程并行检查建立命令通道CrawlCommand队列与结果通道CrawlResult队列启动 N 个工作线程各自持有独立的reqwest::blocking::Client从命令通道recv()取任务调用visit_page把结果send回结果通道主线程负责分发命令、收集结果。一个值得注意的实现细节来自仓库参考解答 link-checker.rsmpsc::Receiver本身不可克隆非Clone要让多个线程共享同一个接收端需要把它包进ArcMutex_再克隆// To multiplex the non-cloneable Receiver, wrap it in ArcMutex_. let command_receiver Arc::new(Mutex::new(command_receiver)); for _ in 0..thread_count { let result_sender result_sender.clone(); let command_receiver Arc::clone(command_receiver); thread::spawn(move || { let client Client::new(); loop { let command_result { let receiver_guard command_receiver.lock().unwrap(); receiver_guard.recv() }; let Ok(crawl_command) command_result else { // The sender got dropped. No more commands coming in. break; }; let crawl_result match visit_page(client, crawl_command) { Ok(link_urls) Ok(link_urls), Err(error) Err((crawl_command.url, error)), }; result_sender.send(crawl_result).unwrap(); } }); }线程退出的判定方式是「发送端被 drop」当命令通道的所有发送端都被释放后recv()返回Err工作线程便退出循环。在参考实现中spawn_crawler_threads默认启动16 个工作线程spawn_crawler_threads(command_receiver, result_sender, 16)。任务二递归爬取同域名页面在并行基础上扩展到递归维护CrawlState记录domain与visited_pagesHashSetStringshould_extract_links判断某 URL 是否属于当前域名mark_visited去重——只有首次访问的页面才会被加入待爬队列struct CrawlState { domain: String, visited_pages: std::collections::HashSetString, } impl CrawlState { fn new(start_url: Url) - CrawlState { let mut visited_pages std::collections::HashSet::new(); visited_pages.insert(start_url.as_str().to_string()); CrawlState { domain: start_url.domain().unwrap().to_string(), visited_pages } } /// Determine whether links within the given page should be extracted. fn should_extract_links(self, url: Url) - bool { url.domain().is_some_and(|d| d self.domain) } /// Mark the given page as visited, returning false if it had already /// been visited. fn mark_visited(mut self, url: Url) - bool { self.visited_pages.insert(url.as_str().to_string()) } }对每个新发现的同域 URL设置extract_links true继续下发对外域 URL 设置extract_links false只检查链接是否有效、不再深入用pending_urls计数器判断爬取何时结束每收到一个结果减一、每下发一个新命令加一归零即全部完成给爬取规模设置上限约 100 页避免被目标站点封禁。参考解答的control_crawl完整实现了上述工作流check_links将命令通道、结果通道与工作线程组装起来main最终打印所有坏链接fn check_links(start_url: Url) - VecUrl { let (result_sender, result_receiver) mpsc::channel::CrawlResult(); let (command_sender, command_receiver) mpsc::channel::CrawlCommand(); spawn_crawler_threads(command_receiver, result_sender, 16); control_crawl(start_url, command_sender, result_receiver) } fn main() { let start_url reqwest::Url::parse(https://www.google.org).unwrap(); let bad_urls check_links(start_url); println!(Bad URLs: {:#?}, bad_urls); }教学提示来自原文档原文档对本练习的定位非常明确这是一个复杂练习旨在让学生获得比以往更大的项目实战机会。本练习的成功标准并非「一遍写对」而是「在某个真实的工程问题上卡住然后在同学或讲师的帮助下把它解决掉」——过程本身即是学习。两个练习的设计关联把两个练习放在同一小节并非偶然它们在知识点上形成递进哲学家就餐问题是「同步原语的最小闭环」只用标准库在 20 行内同时涉及Arc、Mutex、mpsc::sync_channel与死锁分析是理解共享状态模型的最小实验场链接检查器则把同样的原语放大到真实工程16 个线程 两条通道构成的生产者-消费者架构、ArcMutexReceiver的多线程共享技巧、去重与计数驱动的终止条件完整复现了并发任务调度的常见模式。两者共同的底层主线是课程 并发章节 反复强调的Rust 的并发原语保证的是数据竞争安全而并发逻辑的正确性无死锁、任务不丢失、优雅终止依然需要工程师亲手设计。哲学家就餐练习用死锁直观证明了这一点链接检查器则要求你在更大的规模上亲自落实这些设计。自检清单完成练习后可用以下清单核对学习成果哲学家就餐程序cargo run后能稳定跑完 5×100 次循环并打印所有想法不会挂起能解释「最后一位哲学家交换筷子」为什么能消除死锁以及去掉这行代码会发生什么链接检查器能对https://www.google.org/完成全站爬取区分同域链接与外域链接能解释工作线程为何以「发送端被 drop」作为退出信号能说明ArcMutexReceiver的必要性Receiver为何不能直接多线程共享。解答与完整源码可对照仓库 dining-philosophers.rs、link-checker.rs 及 solutions.md。【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表