Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .claude/commands/preflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,18 @@ argument-hint: "[可选: 目标文件或目录;留空则检查已暂存文件]"

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

## 输出

```
## Preflight 报告
- [ ] frontmatter: pass / fail(细节)
- [ ] markdownlint: pass / fail
- [ ] 粗体渲染: pass / fail(N 行 ** 残留)
- [ ] 内部链接: pass / 待检
- [ ] 索引更新: 已更新 / 待补
- [ ] 代码示例: 不涉及 / 待编译确认
Expand Down
34 changes: 34 additions & 0 deletions .claude/style/writing-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,40 @@ cpp_standard: [11, 14, 17, 20]

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

### Lab / 练习骨架

章节大作业(Lab)用 CS144 风格的渐进式项目组织:一个可运行的小系统,拆成 3–5 个 milestone,每个 milestone 只引入一个新的工程问题。重点不在"把文章示例再打一遍",而在让学习者反复面对生命周期、关闭语义、异常传播、测试和性能测量这些真实工程问题。

骨架:

```
# Lab N: {项目名称}

## 目标
## 前置知识
## 工程脚手架(指向 code/ 下可构建工程 + TDD 工作流)
## 最终接口(类型表:成员 | 语义 | 所属 milestone)
## Milestone 1: {名称}
### 目标 / ### 为什么 / ### 实现指引 / ### 验证
## Milestone N: ...
## 性能测试(如适用,引用统一方法论)
## 扩展练习(bonus,明确标注非主线)
## 自查清单
## 参考资源
```

每个 milestone 内部固定四段:**目标**(达成什么)→ **为什么**(在整体设计中的位置)→ **实现指引**(关键数据结构 + 并发注意点 + 踩坑预警 + 骨架代码,不给完整实现)→ **验证**(可编译的 Catch2 测试)。

Lab 写作的铁律:

- **配套工程脚手架**:Lab 必须有 `code/` 下可构建的工程(顶层 CMake + 测试框架 + 每 milestone 独立测试目标),学习者在 `include/` 补全实现、测试逐 milestone 变绿。**不在文章里贴零散代码片段让学习者自己拼**。工程模式参考 `code/volumn_codes/vol5-labs/` 和 `code/volumn_codes/vol9/`。
- **测试即验收**:每个 milestone 的"验证"必须是可编译、学习者补全实现后能变绿的真实 Catch2 测试。测试代码须与"最终接口"和"实现指引"自洽——指引讲什么,测试就测什么,不能错位。
- **TSan 优先**:并发 Lab 的正确性用 ThreadSanitizer 验证(Debug 构建编译期开 `-fsanitize=thread`)。**禁止写 `--tsan` 之类不存在的运行参数**——那是编译期选项,不是 Catch2 运行参数。涉及性能的 Lab 引用 `chapter-projects-outline.md` 的统一性能方法论。
- **指引与代码不得自相矛盾**:实现指引强调的做法,验证代码里必须照此实现或明确说明差异原因。例如指引讲 `thread_local`,要讲清楚它在当前场景与"复用场景"的差异,不能指引强调而代码不用、读者一头雾水。
- **踩坑预警要写实测过的真坑**:并发教程尤其要避免"看起来能跑"的错觉。踩坑预警里的每个坑,作者应当自己踩过或用 TSan/汇编验证过——不编造听起来合理的坑。

Lab 的详细设计依据见 [`.claude/chapter-projects-outline.md`](../chapter-projects-outline.md)(设计大纲、milestone、验收、性能方法论),Lab 生产流程见 [`.claude/prompts/produce-vol5-labs.md`](../prompts/produce-vol5-labs.md)。

## 1.3 代码风格

所有教程代码遵循以下约定,与项目 `.clang-format` 保持一致。
Expand Down
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ repos:
always_run: true
pass_filenames: false

- id: check-bold-rendering
name: Check bold rendering (**)
entry: pnpm exec tsx scripts/check_bold_rendering.ts
language: system
files: '^documents/.*\.md$'
pass_filenames: false

# Check for added large files
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
Expand Down
40 changes: 40 additions & 0 deletions code/volumn_codes/vol5-labs/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 卷五练习手册统一工程
#
# 本顶层只编译 examples/ 下的「参考实现」(作者验证用;CI 跑这些保证参考实现始终可构建、测试全绿)。
# templates/ 是空实现骨架,给初学者拷贝去做练习,不在此编译。
#
# 每个 Lab 工程都是 standalone,可独立构建(IDE/clangd 友好):
# cd examples/lab0_thread_lifecycle # 或 templates/lab0_thread_lifecycle
# cmake -B build -DCMAKE_BUILD_TYPE=Debug
# cmake --build build
#
# 一键构建全部参考实现:
# cmake -B build -DCMAKE_BUILD_TYPE=Debug
# cmake --build build
# ctest --test-dir build --output-on-failure
cmake_minimum_required(VERSION 3.20)

project(Vol5Labs LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

# 顶层拉一次 Catch2,被 add_subdirectory 的 example 复用(FetchContent 全局缓存)。
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.7.1
)
FetchContent_MakeAvailable(Catch2)

enable_testing()

# 编译参考实现做验证。templates/ 不编译(空实现,给初学者拷贝)。
# 注意:若 example 的参考实现尚未补全(include/lab0/ 还是空声明),
# 此处会停在链接阶段报 undefined reference —— 那是预期,提示参考实现待写。
add_subdirectory(examples/lab0_thread_lifecycle)
# 将来新增 Lab 在此追加:
# add_subdirectory(examples/lab1_bounded_queue) # 待 example 参考实现完成取消注释(空声明会停在链接 undefined reference)
# add_subdirectory(examples/lab1_xxx)
83 changes: 83 additions & 0 deletions code/volumn_codes/vol5-labs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# 卷五练习手册 · 工程脚手架

vol5 并发练习手册的可运行代码工程。每个 Lab 有两份:

- **`templates/<lab>/`** — 空实现骨架(声明 + TODO),给初学者**拷贝**去做练习。
- **`examples/<lab>/`** — 参考实现(完整),供卡住时对照;顶层 CMake 编译它做验证。

两份各自是 **standalone 工程**(自己的 CMakeLists + Catch2),能独立打开/构建,IDE/clangd 友好。

## 目录结构

```text
vol5-labs/
├── CMakeLists.txt # 顶层:FetchContent Catch2 + 编译 examples/ 做验证
├── README.md # 本文件
├── templates/
│ └── lab0_thread_lifecycle/ # 模板(空实现,初学者拷贝)
│ ├── CMakeLists.txt # standalone(FetchContent + Catch2)
│ ├── include/lab0/ # ← 你拷贝后在这里补全实现
│ │ ├── file_info.h # 数据结构(已给全,不用改)
│ │ ├── worker_stats.h # 数据结构(已给全,不用改)
│ │ ├── joining_thread.h # Milestone 2 实现
│ │ └── file_scanner.h # Milestone 1/3/4 实现
│ └── test/ # 教程提供的测试(不用改,除非补边界测试)
└── examples/
└── lab0_thread_lifecycle/ # 参考实现(完整,被顶层编译验证)
├── CMakeLists.txt # standalone
├── include/lab0/ # 完整实现
└── test/
```

依赖管理用 **FetchContent**(不再用 CPM),每个 standalone 工程自己拉 Catch2 v3.7.1,无需额外文件。

## 开始一个 Lab(初学者)

```bash
# 1. 拷贝模板去做(也可以直接在 templates/ 里改)
cp -r code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle /tmp/my-lab0
cd /tmp/my-lab0

# 2. 构建(首次 FetchContent 拉 Catch2,需联网)
cmake -B build -DCMAKE_BUILD_TYPE=Debug # Debug 默认开 ThreadSanitizer
cmake --build build
```

第一次构建会停在链接阶段,报 `undefined reference to lab0::FileScanner::scan()` — 这是**故意的**:`file_scanner.h` / `joining_thread.h` 只有声明没有实现,链接器在提醒你"该动手了"。这就是 TDD 式练习的起点。

按对应 Lab 的 handbook(如 `documents/vol5-concurrency/exercises/00-thread-lifecycle.md`)的 Milestone 顺序实现 `include/lab0/*.h`,每完成一个 milestone 跑对应测试,变绿即通过。

## 跑测试

```bash
ctest --test-dir build --output-on-failure # 全部
./build/test/test_milestone1 # 单个 milestone
./build/test/test_milestone2 "[lab0][milestone2]" # Catch2 标签过滤
```

> TSan 不需要额外参数 — Debug 构建已经在编译期通过 `-fsanitize=thread` 开启,直接运行测试即在 TSan 下。Release 构建不开 sanitizer。**注意:Catch2 没有 `--tsan` 这种运行参数,TSan 是编译期选项。**

## 卡住了?

打开 `examples/lab0_thread_lifecycle/` 看参考实现(作者的实现,不一定最优,欢迎 Issue/PR 改进)。但**先自己挣扎一会儿** — 直接抄参考实现会丢掉 dogfooding 的价值。

## 作者 / CI:一键验证参考实现

```bash
cd code/volumn_codes/vol5-labs
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
ctest --test-dir build --output-on-failure
```

顶层编译 `examples/` 下所有参考实现。`templates/` 不编译(空实现,给初学者的)。

## dogfooding 反馈

你是这本手册的一号测试用户。**遇到任何卡点都记下来回报**,据此迭代 handbook 和测试。需要反馈的典型情况:指引不清 / 测试红得不明白 / 踩坑没预警到 / 命令跑不通 / 指引与代码矛盾。

```text
卡点位置:Milestone N / 文件 / 行
现象:……(报错信息 / 实际行为)
期望:……(你觉得应该怎样)
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
cmake_minimum_required(VERSION 3.20)

# 本工程是 standalone:可以 cd 进来独立构建。
# cmake -B build -DCMAKE_BUILD_TYPE=Debug
# cmake --build build
# 被 vol5-labs 顶层 add_subdirectory 包含时也能工作(guard 跳过重复 FetchContent/enable_testing)。
project(lab0_thread_lifecycle LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# 只在 standalone 构建时拉 Catch2 + 开 testing;
# 被顶层 add_subdirectory 包含时由顶层负责(复用顶层的 Catch2)。
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.7.1
)
FetchContent_MakeAvailable(Catch2)
enable_testing()
endif()

# lab0 是 header-only INTERFACE 库,实现写在 include/lab0/*.h:
# - templates/ 下是空实现(声明+TODO),初学者拷贝去补全
# - examples/ 下是参考实现(完整)
add_library(lab0 INTERFACE)
target_include_directories(lab0 INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_features(lab0 INTERFACE cxx_std_17)

add_subdirectory(test)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Paralle File Scanner / 文件并行扫描器样例

这里是第五卷的并发文件扫描器小练习,笔者的实现放在这里了!
实现不一定是最好的,如果您有一些想法或者更好的改进,随意Issue + PR!
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include <cstdint>
#include <filesystem>
#include <string>

namespace lab0 {

/**
* @brief This is the file system paths
*
*/
struct FileInfo {
std::filesystem::path path; ///> where is the file?
std::uintmax_t file_size = 0; ///> how large
std::string extension; ///> Extensions owns the point, like: ".cpp", or ".c"
};

} // namespace lab0
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#pragma once

#include <algorithm>
#include <cstddef>
#include <filesystem>
#include <utility>
#include <vector>

#include "joining_thread.h"
#include "worker_stats.h"

namespace lab0 {

/**
* @brief Core Scanner
*
*/
class FileScanner {
public:
FileScanner(std::filesystem::path root, std::size_t num_workers)
: root_path_(std::move(root)), num_workers_(num_workers) {}

WorkerStats scan() {
namespace fs = std::filesystem;

// 主线程收集整棵目录树的 regular_file(recursive 默认不跟随 symlink)
std::vector<fs::directory_entry> entries;
for (const auto& entry : fs::recursive_directory_iterator(root_path_)) {
if (entry.is_regular_file()) {
entries.emplace_back(entry);
}
}

WorkerStats stats;
const auto files_count = entries.size();
if (files_count == 0) {
return stats; // 空目录直接返回
}

// 11 files, 3 workers -> each get 4 4 3(有余数则前面的 worker 多拿一个)
auto each_file_cnt = files_count / num_workers_;
if (files_count % num_workers_ != 0) {
each_file_cnt += 1;
}
const auto worker_count = (files_count + each_file_cnt - 1) / each_file_cnt; // ceil

// MS4: 每 worker 一个局部 WorkerStats, 写回 results[worker_id] 独立槽位。
// 不同 worker 写不同槽位 -> 无竞争, 不再需要 mutex。
// (必须预分配大小, 否则 emplace 触发 reallocate 会和并发 worker 抢内存)
std::vector<WorkerStats> results(worker_count);

{
// MS2: JoiningThread 替换裸 std::thread, 作用域结束自动 join。
std::vector<JoiningThread> workers;
workers.reserve(worker_count);
for (std::size_t worker_id = 0; worker_id < worker_count; ++worker_id) {
const auto start = worker_id * each_file_cnt;
const auto end = std::min(files_count, start + each_file_cnt);
workers.emplace_back([worker_id, start, end, &entries, &results]() {
WorkerStats local;
for (std::size_t index = start; index < end; ++index) {
local.files_scanned++;
local.total_bytes += entries[index].file_size();
local.ext_counts[entries[index].path().extension()]++;
}
results[worker_id] = std::move(local);
});
}
} // <- workers 在此析构 join; 必须在汇总 results 之前(MS4 踩坑:join 时机)

for (const auto& s : results) {
stats += s;
}
return stats;
}

private:
std::filesystem::path root_path_;
std::size_t num_workers_;
};

} // namespace lab0
Loading
Loading