|
| 1 | +Title: KAIST CS492 - 并发编程 · 课程速读(Part3. 作业1 - 并行Web服务器) |
| 2 | +Date: 2025-10-07 10:00 |
| 3 | +Tags: 并行计算, 并发, Rust, 系统编程 |
| 4 | +Slug: kaist-cs492-3 |
| 5 | + |
| 6 | +[课程主页](https://github.com/kaist-cp/cs431) |
| 7 | + |
| 8 | +## 作业 1:**并行 Web 服务器(Parallel Web Server)** |
| 9 | + |
| 10 | +在这一节课中,课程正式布置了第一个编程作业——实现一个“并行 Web 服务器”。这个作业不仅是对并发与线程管理的实践练习,也是对课程前面关于锁、线程、共享内存和同步机制理解的首次综合应用。 |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## 一、作业目标 |
| 15 | + |
| 16 | +你的任务是用 **Rust** 实现一个可以同时处理多个请求的并行 Web 服务器。这个服务器能够: |
| 17 | + |
| 18 | +1. 监听本地端口(默认 7878); |
| 19 | +2. 响应浏览器访问请求; |
| 20 | +3. 对请求执行“耗时计算”,并将结果缓存; |
| 21 | +4. 支持多线程并行处理; |
| 22 | +5. 能够优雅地终止(当按下 Ctrl+C 时,打印访问统计信息)。 |
| 23 | + |
| 24 | +通过这个作业,你将学习如何将**多线程、通道通信、任务调度和缓存机制**组合起来,构建一个真正的并行程序。 |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## 二、运行方式与预期行为 |
| 29 | + |
| 30 | +课程提供了作业框架。编译并运行服务器: |
| 31 | + |
| 32 | +``` |
| 33 | +cargo run -- hello-server |
| 34 | +``` |
| 35 | + |
| 36 | +程序启动后,会输出启动日志。当你打开浏览器并访问以下地址: |
| 37 | + |
| 38 | +``` |
| 39 | +http://localhost:7878/ |
| 40 | +``` |
| 41 | + |
| 42 | +如果访问根目录,会得到一个错误页面; |
| 43 | +如果访问某个用户名(例如 `http://localhost:7878/alice`),程序会执行一次“昂贵的计算”(模拟服务器负载),几秒钟后返回结果。之后再次访问相同页面,响应将立即返回——因为结果被缓存了。 |
| 44 | + |
| 45 | +访问不同的用户(如 `bob`)时,服务器会再次计算一次,然后也缓存结果。 |
| 46 | + |
| 47 | +按下 **Ctrl + C** 退出服务器后,程序会打印出访问统计,例如: |
| 48 | + |
| 49 | +``` |
| 50 | +Statistics: |
| 51 | + /alice visited 7 times |
| 52 | + /bob visited 5 times |
| 53 | + /error visited 4 times |
| 54 | +``` |
| 55 | + |
| 56 | +这表示服务器在运行期间记录了每个页面的访问次数。 |
| 57 | + |
| 58 | +--- |
| 59 | + |
| 60 | +## 三、系统结构概览 |
| 61 | + |
| 62 | +该 Web 服务器由多个线程协同组成,每个线程负责不同功能。整体结构包括: |
| 63 | + |
| 64 | +1. **主线程(Main Thread)** |
| 65 | + 负责创建各个组件(线程池、监听器、通道等)并启动系统。 |
| 66 | + |
| 67 | +2. **监听线程(Listener)** |
| 68 | + 通过 TCP 监听端口,接收来自客户端的连接请求。 |
| 69 | + |
| 70 | +3. **工作线程(Worker Threads)** |
| 71 | + 来自线程池(Thread Pool),负责处理具体请求任务。 |
| 72 | + |
| 73 | +4. **报告线程(Reporter)** |
| 74 | + 接收各个请求处理结果,并汇总访问统计。 |
| 75 | + |
| 76 | +5. **取消机制(Cancelable Listener)** |
| 77 | + 通过原子变量(AtomicBool)实现安全终止,当按下 Ctrl+C 时,所有线程能有序退出。 |
| 78 | + |
| 79 | +这些线程之间通过 **通道(Channel)** 通信,实现消息传递与同步。Rust 使用 `crossbeam` crate 提供的有界与无界通道实现高效并发通信。 |
| 80 | + |
| 81 | +--- |
| 82 | + |
| 83 | +## 四、需要你实现的部分 |
| 84 | + |
| 85 | +虽然框架代码已经提供了主要逻辑,但你需要亲手完成三个关键组件: |
| 86 | + |
| 87 | +### 1. Cache:结果缓存模块 |
| 88 | + |
| 89 | +缓存是一个简单的键值存储,用于避免重复计算。 |
| 90 | + |
| 91 | +**功能要求:** |
| 92 | + |
| 93 | +* 提供 `get_or_insert_with()` 接口; |
| 94 | +* 若 key 已存在,直接返回缓存结果; |
| 95 | +* 若不存在,则执行传入的函数(模拟昂贵计算),并将结果存入缓存后返回; |
| 96 | +* 要确保多线程环境下的安全访问(可使用互斥锁或并发结构)。 |
| 97 | + |
| 98 | +该模块考验你对**共享可变状态和锁机制**的理解。 |
| 99 | + |
| 100 | +--- |
| 101 | + |
| 102 | +### 2. Cancelable TCP Listener:可取消的监听器 |
| 103 | + |
| 104 | +Rust 标准库提供了 `TcpListener`,但你需要在其基础上实现一个可取消版本。 |
| 105 | + |
| 106 | +**功能要求:** |
| 107 | + |
| 108 | +* 拥有与标准 `TcpListener` 相同的接口; |
| 109 | +* 增加一个 `cancel()` 方法,当调用后,所有正在阻塞的 `accept()` 调用必须被唤醒并退出; |
| 110 | +* 使用一个 `AtomicBool` 变量表示监听器是否被取消; |
| 111 | +* 调用 `cancel()` 时使用 **Release** 内存序; |
| 112 | +* 调用 `next()`(即 `accept()`)时使用 **Acquire** 内存序; |
| 113 | +* 如果监听器被取消,`next()` 应返回 `None`; |
| 114 | +* 需实现唤醒机制,让被阻塞的监听线程及时检测到取消信号。 |
| 115 | + |
| 116 | +这部分是作业中最具技术含量的部分之一,它结合了**原子操作、内存模型**和**线程同步**,为后续课程打下基础。 |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +### 3. Thread Pool:线程池实现 |
| 121 | + |
| 122 | +线程池管理一组可复用的工作线程。与直接创建新线程相比,它能显著提升性能。 |
| 123 | + |
| 124 | +**功能要求:** |
| 125 | + |
| 126 | +* 支持创建固定大小的线程池; |
| 127 | +* 提供 `execute()` 方法提交任务; |
| 128 | +* 提供 `join()` 方法等待所有任务完成; |
| 129 | +* 使用通道在主线程与工作线程之间分发任务; |
| 130 | +* 在实现中维护一个“正在执行任务的计数器”,当任务数归零时 `join()` 才能返回。 |
| 131 | + |
| 132 | +Rust 官方文档与《The Rust Book》中都有类似实现,可以参考其思想,但需要根据课程框架适当调整。 |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +## 五、关键技术要点 |
| 137 | + |
| 138 | +1. **并行与异步的区别** |
| 139 | + 本作业是并行程序,不使用异步框架(如 tokio)。并行通过线程实现;异步则是通过任务调度器模拟并发。 |
| 140 | + |
| 141 | +2. **通道通信(Channels)** |
| 142 | + 所有线程间通信都基于消息传递而非共享内存,从而避免显式锁竞争。 |
| 143 | + |
| 144 | +3. **原子操作与内存序(Memory Ordering)** |
| 145 | + Release/Acquire 模型保证取消信号在多核环境下的正确传播。 |
| 146 | + |
| 147 | +4. **线程生命周期与 join 逻辑** |
| 148 | + 合理设计线程退出机制,防止僵尸线程或提前释放。 |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## 六、实现建议 |
| 153 | + |
| 154 | +* 从最简单的模块开始(如 Cache); |
| 155 | +* 在实现 Cancelable Listener 时,重点理解 **AtomicBool** 的使用; |
| 156 | +* Thread Pool 可参考官方文档中的示例,关注任务分发与 join 实现; |
| 157 | +* 利用 `cargo test` 验证各部分功能; |
| 158 | +* 完成后,用浏览器实际访问页面测试缓存与并行行为。 |
| 159 | + |
| 160 | +--- |
| 161 | + |
| 162 | +## 七、总结 |
| 163 | + |
| 164 | +这个作业不仅是一次代码练习,更是一次系统设计的训练。它让你体会到: |
| 165 | + |
| 166 | +* 并发程序的结构化设计; |
| 167 | +* 多线程之间的协作与通信; |
| 168 | +* Rust 类型系统在安全并发中的作用; |
| 169 | +* 从共享内存到消息传递的编程哲学转变。 |
| 170 | + |
| 171 | +最终,你将得到一个真正“并行”的 Web 服务器—— |
| 172 | +一个能同时响应多个请求、具备缓存能力、并能优雅关闭的高性能程序。 |
| 173 | + |
| 174 | +这正是现代并发系统设计的缩影。 |
| 175 | + |
| 176 | +> AI生成的[示例代码](https://github.com/Wizmann/KAIST-CS492/tree/main/hw01),与题目要求功能类似,细节可能有差异 |
0 commit comments