Skip to content

Commit 3a43ab4

Browse files
feature: redesigned the concurrency labs (#70)
* feat: 新增粗体渲染检查(防全角标点边界致 ** 失效) check_bold_rendering.ts 用 markdown-it 检测 ** 渲染残留, 加到 preflight + pre-commit。注: vitepress bundle 了 markdown-it 致 require 不到, 脚本暂 exit 0 跳过, 待加 markdown-it 依赖根本修。 * feat(vol5): 练习手册重构 templates/examples 双工程 + Lab 0/1 vol5-labs 改名提平级(volN-labs)。templates(空骨架)+examples(参考实现)各 standalone FetchContent(弃 CPM)。Lab 0 完整参考实现+handbook; Lab 1 模板+6 milestone test+handbook。writing-style 沉淀 Lab 骨架+别被测试骗了。build_examples 适配 vol5-labs。 * feat(ci): build_examples 加 ctest 验证 example 测试绿 build_project 在 build 后检测 CTestTestfile.cmake: 有测试的工程(目前 vol5-labs)跑 ctest, 没配 CTest 的纯示例跳过、不误伤。CI 现在验证 example 参考实现测试全绿。 * fix: ci issue * fix: ci issue
1 parent b7a2e3d commit 3a43ab4

54 files changed

Lines changed: 2462 additions & 1281 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/preflight.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@ argument-hint: "[可选: 目标文件或目录;留空则检查已暂存文件]"
1515

1616
1. **frontmatter 校验**:`.venv/bin/python scripts/validate_frontmatter.py`(**必须用 venv**,系统 python 缺 PyYAML 会全量误报)。
1717
2. **markdownlint**:`markdownlint <目标 .md 文件>`
18-
3. **内部链接**:运行项目的 link check(若有,如 CI 用的脚本),或人工抽检新增 / 改动链接是否可达、`ChapterLink``href` 不以 `.md` 结尾等。
19-
4. **索引检查**:若新增或移动了文章,对应卷 `index.md` 是否已更新链接。
20-
5. **代码示例**(若涉及 `code/`):提醒确认可独立编译(无根 CMakeLists,逐目录构建)。
18+
3. **粗体渲染**:`tsx scripts/check_bold_rendering.ts`(扫 `documents/` 检测 `**` 因标点边界未渲染成 `<strong>` 而字面残留的行;典型是 `**术语(英文)**` 这类。发现即修:让 `**` 边界落在文字上,标点移到 `**` 外)。
19+
4. **内部链接**:运行项目的 link check(若有,如 CI 用的脚本),或人工抽检新增 / 改动链接是否可达、`ChapterLink``href` 不以 `.md` 结尾等。
20+
5. **索引检查**:若新增或移动了文章,对应卷 `index.md` 是否已更新链接。
21+
6. **代码示例**(若涉及 `code/`):提醒确认可独立编译(无根 CMakeLists,逐目录构建)。
2122

2223
## 输出
2324

2425
```
2526
## Preflight 报告
2627
- [ ] frontmatter: pass / fail(细节)
2728
- [ ] markdownlint: pass / fail
29+
- [ ] 粗体渲染: pass / fail(N 行 ** 残留)
2830
- [ ] 内部链接: pass / 待检
2931
- [ ] 索引更新: 已更新 / 待补
3032
- [ ] 代码示例: 不涉及 / 待编译确认

.claude/style/writing-style.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,40 @@ cpp_standard: [11, 14, 17, 20]
143143

144144
决策 / 入口型文章(如「容器选择指南」)是这套骨架的变体:以复杂度对比表 + 内存局部性 + 迭代器失效速查 + 决策树为主,不必强行「上手跑一跑」,但对比数据仍需权威出处(cppreference / 实测)。
145145

146+
### Lab / 练习骨架
147+
148+
章节大作业(Lab)用 CS144 风格的渐进式项目组织:一个可运行的小系统,拆成 3–5 个 milestone,每个 milestone 只引入一个新的工程问题。重点不在"把文章示例再打一遍",而在让学习者反复面对生命周期、关闭语义、异常传播、测试和性能测量这些真实工程问题。
149+
150+
骨架:
151+
152+
```
153+
# Lab N: {项目名称}
154+
155+
## 目标
156+
## 前置知识
157+
## 工程脚手架(指向 code/ 下可构建工程 + TDD 工作流)
158+
## 最终接口(类型表:成员 | 语义 | 所属 milestone)
159+
## Milestone 1: {名称}
160+
### 目标 / ### 为什么 / ### 实现指引 / ### 验证
161+
## Milestone N: ...
162+
## 性能测试(如适用,引用统一方法论)
163+
## 扩展练习(bonus,明确标注非主线)
164+
## 自查清单
165+
## 参考资源
166+
```
167+
168+
每个 milestone 内部固定四段:**目标**(达成什么)→ **为什么**(在整体设计中的位置)→ **实现指引**(关键数据结构 + 并发注意点 + 踩坑预警 + 骨架代码,不给完整实现)→ **验证**(可编译的 Catch2 测试)。
169+
170+
Lab 写作的铁律:
171+
172+
- **配套工程脚手架**:Lab 必须有 `code/` 下可构建的工程(顶层 CMake + 测试框架 + 每 milestone 独立测试目标),学习者在 `include/` 补全实现、测试逐 milestone 变绿。**不在文章里贴零散代码片段让学习者自己拼**。工程模式参考 `code/volumn_codes/vol5-labs/``code/volumn_codes/vol9/`
173+
- **测试即验收**:每个 milestone 的"验证"必须是可编译、学习者补全实现后能变绿的真实 Catch2 测试。测试代码须与"最终接口"和"实现指引"自洽——指引讲什么,测试就测什么,不能错位。
174+
- **TSan 优先**:并发 Lab 的正确性用 ThreadSanitizer 验证(Debug 构建编译期开 `-fsanitize=thread`)。**禁止写 `--tsan` 之类不存在的运行参数**——那是编译期选项,不是 Catch2 运行参数。涉及性能的 Lab 引用 `chapter-projects-outline.md` 的统一性能方法论。
175+
- **指引与代码不得自相矛盾**:实现指引强调的做法,验证代码里必须照此实现或明确说明差异原因。例如指引讲 `thread_local`,要讲清楚它在当前场景与"复用场景"的差异,不能指引强调而代码不用、读者一头雾水。
176+
- **踩坑预警要写实测过的真坑**:并发教程尤其要避免"看起来能跑"的错觉。踩坑预警里的每个坑,作者应当自己踩过或用 TSan/汇编验证过——不编造听起来合理的坑。
177+
178+
Lab 的详细设计依据见 [`.claude/chapter-projects-outline.md`](../chapter-projects-outline.md)(设计大纲、milestone、验收、性能方法论),Lab 生产流程见 [`.claude/prompts/produce-vol5-labs.md`](../prompts/produce-vol5-labs.md)
179+
146180
## 1.3 代码风格
147181

148182
所有教程代码遵循以下约定,与项目 `.clang-format` 保持一致。

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ repos:
3535
always_run: true
3636
pass_filenames: false
3737

38+
- id: check-bold-rendering
39+
name: Check bold rendering (**)
40+
entry: pnpm exec tsx scripts/check_bold_rendering.ts
41+
language: system
42+
files: '^documents/.*\.md$'
43+
pass_filenames: false
44+
3845
# Check for added large files
3946
- repo: https://github.com/pre-commit/pre-commit-hooks
4047
rev: v4.5.0
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# 卷五练习手册统一工程
2+
#
3+
# 本顶层只编译 examples/ 下的「参考实现」(作者验证用;CI 跑这些保证参考实现始终可构建、测试全绿)。
4+
# templates/ 是空实现骨架,给初学者拷贝去做练习,不在此编译。
5+
#
6+
# 每个 Lab 工程都是 standalone,可独立构建(IDE/clangd 友好):
7+
# cd examples/lab0_thread_lifecycle # 或 templates/lab0_thread_lifecycle
8+
# cmake -B build -DCMAKE_BUILD_TYPE=Debug
9+
# cmake --build build
10+
#
11+
# 一键构建全部参考实现:
12+
# cmake -B build -DCMAKE_BUILD_TYPE=Debug
13+
# cmake --build build
14+
# ctest --test-dir build --output-on-failure
15+
cmake_minimum_required(VERSION 3.20)
16+
17+
project(Vol5Labs LANGUAGES CXX)
18+
19+
set(CMAKE_CXX_STANDARD 17)
20+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
21+
set(CMAKE_CXX_EXTENSIONS OFF)
22+
23+
# 顶层拉一次 Catch2,被 add_subdirectory 的 example 复用(FetchContent 全局缓存)。
24+
include(FetchContent)
25+
FetchContent_Declare(
26+
Catch2
27+
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
28+
GIT_TAG v3.7.1
29+
)
30+
FetchContent_MakeAvailable(Catch2)
31+
32+
enable_testing()
33+
34+
# 编译参考实现做验证。templates/ 不编译(空实现,给初学者拷贝)。
35+
# 注意:若 example 的参考实现尚未补全(include/lab0/ 还是空声明),
36+
# 此处会停在链接阶段报 undefined reference —— 那是预期,提示参考实现待写。
37+
add_subdirectory(examples/lab0_thread_lifecycle)
38+
# 将来新增 Lab 在此追加:
39+
# add_subdirectory(examples/lab1_bounded_queue) # 待 example 参考实现完成取消注释(空声明会停在链接 undefined reference)
40+
# add_subdirectory(examples/lab1_xxx)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# 卷五练习手册 · 工程脚手架
2+
3+
vol5 并发练习手册的可运行代码工程。每个 Lab 有两份:
4+
5+
- **`templates/<lab>/`** — 空实现骨架(声明 + TODO),给初学者**拷贝**去做练习。
6+
- **`examples/<lab>/`** — 参考实现(完整),供卡住时对照;顶层 CMake 编译它做验证。
7+
8+
两份各自是 **standalone 工程**(自己的 CMakeLists + Catch2),能独立打开/构建,IDE/clangd 友好。
9+
10+
## 目录结构
11+
12+
```text
13+
vol5-labs/
14+
├── CMakeLists.txt # 顶层:FetchContent Catch2 + 编译 examples/ 做验证
15+
├── README.md # 本文件
16+
├── templates/
17+
│ └── lab0_thread_lifecycle/ # 模板(空实现,初学者拷贝)
18+
│ ├── CMakeLists.txt # standalone(FetchContent + Catch2)
19+
│ ├── include/lab0/ # ← 你拷贝后在这里补全实现
20+
│ │ ├── file_info.h # 数据结构(已给全,不用改)
21+
│ │ ├── worker_stats.h # 数据结构(已给全,不用改)
22+
│ │ ├── joining_thread.h # Milestone 2 实现
23+
│ │ └── file_scanner.h # Milestone 1/3/4 实现
24+
│ └── test/ # 教程提供的测试(不用改,除非补边界测试)
25+
└── examples/
26+
└── lab0_thread_lifecycle/ # 参考实现(完整,被顶层编译验证)
27+
├── CMakeLists.txt # standalone
28+
├── include/lab0/ # 完整实现
29+
└── test/
30+
```
31+
32+
依赖管理用 **FetchContent**(不再用 CPM),每个 standalone 工程自己拉 Catch2 v3.7.1,无需额外文件。
33+
34+
## 开始一个 Lab(初学者)
35+
36+
```bash
37+
# 1. 拷贝模板去做(也可以直接在 templates/ 里改)
38+
cp -r code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle /tmp/my-lab0
39+
cd /tmp/my-lab0
40+
41+
# 2. 构建(首次 FetchContent 拉 Catch2,需联网)
42+
cmake -B build -DCMAKE_BUILD_TYPE=Debug # Debug 默认开 ThreadSanitizer
43+
cmake --build build
44+
```
45+
46+
第一次构建会停在链接阶段,报 `undefined reference to lab0::FileScanner::scan()` — 这是**故意的**:`file_scanner.h` / `joining_thread.h` 只有声明没有实现,链接器在提醒你"该动手了"。这就是 TDD 式练习的起点。
47+
48+
按对应 Lab 的 handbook(如 `documents/vol5-concurrency/exercises/00-thread-lifecycle.md`)的 Milestone 顺序实现 `include/lab0/*.h`,每完成一个 milestone 跑对应测试,变绿即通过。
49+
50+
## 跑测试
51+
52+
```bash
53+
ctest --test-dir build --output-on-failure # 全部
54+
./build/test/test_milestone1 # 单个 milestone
55+
./build/test/test_milestone2 "[lab0][milestone2]" # Catch2 标签过滤
56+
```
57+
58+
> TSan 不需要额外参数 — Debug 构建已经在编译期通过 `-fsanitize=thread` 开启,直接运行测试即在 TSan 下。Release 构建不开 sanitizer。**注意:Catch2 没有 `--tsan` 这种运行参数,TSan 是编译期选项。**
59+
60+
## 卡住了?
61+
62+
打开 `examples/lab0_thread_lifecycle/` 看参考实现(作者的实现,不一定最优,欢迎 Issue/PR 改进)。但**先自己挣扎一会儿** — 直接抄参考实现会丢掉 dogfooding 的价值。
63+
64+
## 作者 / CI:一键验证参考实现
65+
66+
```bash
67+
cd code/volumn_codes/vol5-labs
68+
cmake -B build -DCMAKE_BUILD_TYPE=Debug
69+
cmake --build build
70+
ctest --test-dir build --output-on-failure
71+
```
72+
73+
顶层编译 `examples/` 下所有参考实现。`templates/` 不编译(空实现,给初学者的)。
74+
75+
## dogfooding 反馈
76+
77+
你是这本手册的一号测试用户。**遇到任何卡点都记下来回报**,据此迭代 handbook 和测试。需要反馈的典型情况:指引不清 / 测试红得不明白 / 踩坑没预警到 / 命令跑不通 / 指引与代码矛盾。
78+
79+
```text
80+
卡点位置:Milestone N / 文件 / 行
81+
现象:……(报错信息 / 实际行为)
82+
期望:……(你觉得应该怎样)
83+
```
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
cmake_minimum_required(VERSION 3.20)
2+
3+
# 本工程是 standalone:可以 cd 进来独立构建。
4+
# cmake -B build -DCMAKE_BUILD_TYPE=Debug
5+
# cmake --build build
6+
# 被 vol5-labs 顶层 add_subdirectory 包含时也能工作(guard 跳过重复 FetchContent/enable_testing)。
7+
project(lab0_thread_lifecycle LANGUAGES CXX)
8+
9+
set(CMAKE_CXX_STANDARD 17)
10+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
11+
set(CMAKE_CXX_EXTENSIONS OFF)
12+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
13+
14+
# 只在 standalone 构建时拉 Catch2 + 开 testing;
15+
# 被顶层 add_subdirectory 包含时由顶层负责(复用顶层的 Catch2)。
16+
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
17+
include(FetchContent)
18+
FetchContent_Declare(
19+
Catch2
20+
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
21+
GIT_TAG v3.7.1
22+
)
23+
FetchContent_MakeAvailable(Catch2)
24+
enable_testing()
25+
endif()
26+
27+
# lab0 是 header-only INTERFACE 库,实现写在 include/lab0/*.h:
28+
# - templates/ 下是空实现(声明+TODO),初学者拷贝去补全
29+
# - examples/ 下是参考实现(完整)
30+
add_library(lab0 INTERFACE)
31+
target_include_directories(lab0 INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
32+
target_compile_features(lab0 INTERFACE cxx_std_17)
33+
34+
add_subdirectory(test)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Paralle File Scanner / 文件并行扫描器样例
2+
3+
这里是第五卷的并发文件扫描器小练习,笔者的实现放在这里了!
4+
实现不一定是最好的,如果您有一些想法或者更好的改进,随意Issue + PR!
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
#include <filesystem>
5+
#include <string>
6+
7+
namespace lab0 {
8+
9+
/**
10+
* @brief This is the file system paths
11+
*
12+
*/
13+
struct FileInfo {
14+
std::filesystem::path path; ///> where is the file?
15+
std::uintmax_t file_size = 0; ///> how large
16+
std::string extension; ///> Extensions owns the point, like: ".cpp", or ".c"
17+
};
18+
19+
} // namespace lab0
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#pragma once
2+
3+
#include <algorithm>
4+
#include <cstddef>
5+
#include <filesystem>
6+
#include <utility>
7+
#include <vector>
8+
9+
#include "joining_thread.h"
10+
#include "worker_stats.h"
11+
12+
namespace lab0 {
13+
14+
/**
15+
* @brief Core Scanner
16+
*
17+
*/
18+
class FileScanner {
19+
public:
20+
FileScanner(std::filesystem::path root, std::size_t num_workers)
21+
: root_path_(std::move(root)), num_workers_(num_workers) {}
22+
23+
WorkerStats scan() {
24+
namespace fs = std::filesystem;
25+
26+
// 主线程收集整棵目录树的 regular_file(recursive 默认不跟随 symlink)
27+
std::vector<fs::directory_entry> entries;
28+
for (const auto& entry : fs::recursive_directory_iterator(root_path_)) {
29+
if (entry.is_regular_file()) {
30+
entries.emplace_back(entry);
31+
}
32+
}
33+
34+
WorkerStats stats;
35+
const auto files_count = entries.size();
36+
if (files_count == 0) {
37+
return stats; // 空目录直接返回
38+
}
39+
40+
// 11 files, 3 workers -> each get 4 4 3(有余数则前面的 worker 多拿一个)
41+
auto each_file_cnt = files_count / num_workers_;
42+
if (files_count % num_workers_ != 0) {
43+
each_file_cnt += 1;
44+
}
45+
const auto worker_count = (files_count + each_file_cnt - 1) / each_file_cnt; // ceil
46+
47+
// MS4: 每 worker 一个局部 WorkerStats, 写回 results[worker_id] 独立槽位。
48+
// 不同 worker 写不同槽位 -> 无竞争, 不再需要 mutex。
49+
// (必须预分配大小, 否则 emplace 触发 reallocate 会和并发 worker 抢内存)
50+
std::vector<WorkerStats> results(worker_count);
51+
52+
{
53+
// MS2: JoiningThread 替换裸 std::thread, 作用域结束自动 join。
54+
std::vector<JoiningThread> workers;
55+
workers.reserve(worker_count);
56+
for (std::size_t worker_id = 0; worker_id < worker_count; ++worker_id) {
57+
const auto start = worker_id * each_file_cnt;
58+
const auto end = std::min(files_count, start + each_file_cnt);
59+
workers.emplace_back([worker_id, start, end, &entries, &results]() {
60+
WorkerStats local;
61+
for (std::size_t index = start; index < end; ++index) {
62+
local.files_scanned++;
63+
local.total_bytes += entries[index].file_size();
64+
local.ext_counts[entries[index].path().extension()]++;
65+
}
66+
results[worker_id] = std::move(local);
67+
});
68+
}
69+
} // <- workers 在此析构 join; 必须在汇总 results 之前(MS4 踩坑:join 时机)
70+
71+
for (const auto& s : results) {
72+
stats += s;
73+
}
74+
return stats;
75+
}
76+
77+
private:
78+
std::filesystem::path root_path_;
79+
std::size_t num_workers_;
80+
};
81+
82+
} // namespace lab0

0 commit comments

Comments
 (0)