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
6 changes: 6 additions & 0 deletions documents/bookmarks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: "书签"
description: "收藏的文章与摘录的便签都在这里——续读、跳回原文原处、导出导入备份"
---

<BookmarkList />
9 changes: 9 additions & 0 deletions documents/en/bookmarks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: "Bookmarks"
description: "Your saved articles and highlighted notes live here — resume reading, jump back to the exact passage, export/import a backup"
translation:
source: documents/bookmarks.md
source_hash: 964ebc328726e597852aaee288a2c8c4a350ea148065b1639ce3e2cc89b46457
---

<BookmarkList />
14 changes: 7 additions & 7 deletions documents/en/vol5-concurrency/exercises/00-thread-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ related:
- "Thread Ownership and RAII"
translation:
source: documents/vol5-concurrency/exercises/00-thread-lifecycle.md
source_hash: ff4f57476dec5b5d89b2ce4d45333b7aa37f6a7714a8be66b5ffee096c7fea97
source_hash: a0dd59669eea75ce22481a7d0419d3ff2c5e84f4e216a933a830c20cbe1cdebb
translated_at: '2026-09-26T09:17:12+00:00'
engine: anthropic
token_count: 11800
---
# Lab 0: Thread Lifecycle

> The runnable project that goes with this Lab lives at [`code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/`](../../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/). Expect roughly **4–6 hours** of hands-on work (`reading_time_minutes` counts pure reading minutes, not hands-on time).
> The runnable project that goes with this Lab lives at `code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/`. Expect roughly **4–6 hours** of hands-on work (`reading_time_minutes` counts pure reading minutes, not hands-on time).

## Objectives

Expand Down Expand Up @@ -71,7 +71,7 @@ templates/lab0_thread_lifecycle/
└── test_milestone1.cpp … test_milestone4.cpp
```

Build instructions for the entire `vol5-labs/` directory, plus the dogfooding feedback process, live in [`vol5-labs/README.md`](../../../../code/volumn_codes/vol5-labs/README.md). Read it first.
Build instructions for the entire `vol5-labs/` directory, plus the dogfooding feedback process, live in `vol5-labs/README.md`. Read it first.

First build (requires internet access; FetchContent pulls Catch2 v3):

Expand Down Expand Up @@ -181,7 +181,7 @@ For statistics, start with the simplest thing possible: global `std::atomic<std:

### Verification

The corresponding tests live in [`test/test_milestone1.cpp`](../../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone1.cpp), covering three scenarios: the scan collects all files, an empty directory doesn't crash, and the total byte count is correct. Key assertion:
The corresponding tests live in `test/test_milestone1.cpp`, covering three scenarios: the scan collects all files, an empty directory doesn't crash, and the total byte count is correct. Key assertion:

```cpp
TEST_CASE("MS1: scan collects all files", "[lab0][milestone1]") {
Expand Down Expand Up @@ -228,7 +228,7 @@ Once `JoiningThread` is done, go back to `file_scanner.h`, swap `std::vector<std

> **Don't let the tests fool you**: `test_milestone2` only tests the `JoiningThread` class itself (decoupled from `FileScanner`) and **does not check whether `scan()` actually uses it**. So even with `JoiningThread` implemented and every test green, if `scan()` still uses raw `std::thread`s plus a manual `join()` loop, this milestone has not truly been completed. **The real acceptance criteria: no manual `join()` loop visible in `scan()`, and the thread container is `std::vector<lab0::JoiningThread>`.**

[`test/test_milestone2.cpp`](../../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone2.cpp) only tests `JoiningThread` itself (decoupled from `FileScanner`), covering four scenarios: automatic join at scope exit, all workers still joined on an exception path, ownership transferred by move, and `vector` destruction joining everything. Pay attention to the exception-path one:
`test/test_milestone2.cpp` only tests `JoiningThread` itself (decoupled from `FileScanner`), covering four scenarios: automatic join at scope exit, all workers still joined on an exception path, ownership transferred by move, and `vector` destruction joining everything. Pay attention to the exception-path one:

```cpp
TEST_CASE("MS2: exception path still joins all workers", "[lab0][milestone2]") {
Expand Down Expand Up @@ -272,7 +272,7 @@ In MS1 we passed the file path list to workers by value — which is in fact alr

### Verification

[`test/test_milestone3.cpp`](../../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone3.cpp) verifies: non-divisible splits still cover all files (30 files / 8 workers), prime file counts (17 files) lose nothing under any split, and move-only types (`unique_ptr`) can be passed into threads safely. Take the prime case:
`test/test_milestone3.cpp` verifies: non-divisible splits still cover all files (30 files / 8 workers), prime file counts (17 files) lose nothing under any split, and move-only types (`unique_ptr`) can be passed into threads safely. Take the prime case:

```cpp
TEST_CASE("MS3: prime file count covered by any worker count", "[lab0][milestone3]") {
Expand Down Expand Up @@ -330,7 +330,7 @@ One more small point: the `worker_id` in `results[worker_id]` must be unique per

> **Don't let the tests fool you**: `test_milestone4` only checks that the result numbers are right (matching the single-threaded run); it **does not check whether the statistics are genuinely "local per worker"**. So even with every test green, if `scan()` still does its statistics through a shared `mutex`/`atomic`, you are really still at MS1 and this milestone has not truly been completed. **The real acceptance criteria: no locks and no shared atomics in `scan()`; statistics go through independent `results[worker_id]` slots plus main-thread aggregation.**

[`test/test_milestone4.cpp`](../../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone4.cpp) verifies: a multi-threaded scan's results are **exactly identical** to a single-threaded file-by-file scan (all three of file count, byte count, and extension distribution must match), plus a stress test of 200 files / 8 workers. Key assertion:
`test/test_milestone4.cpp` verifies: a multi-threaded scan's results are **exactly identical** to a single-threaded file-by-file scan (all three of file count, byte count, and extension distribution must match), plus a stress test of 200 files / 8 workers. Key assertion:

```cpp
TEST_CASE("MS4: multi-threaded stats match single-threaded baseline", "[lab0][milestone4]") {
Expand Down
6 changes: 3 additions & 3 deletions documents/en/vol5-concurrency/exercises/01-bounded-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ related:
- 'latch, barrier, and semaphore'
translation:
source: documents/vol5-concurrency/exercises/01-bounded-queue.md
source_hash: 956f42289afc87049bb47f380040c1b6681e68a1a1c502c030dcf97530a0cb5c
source_hash: 2d5b3222401ff2c5b5d79f833d783a8940074148bc9588a893a31dc250cf4617
translated_at: '2026-09-26T09:22:45+00:00'
engine: anthropic
token_count: 8200
---

# Lab 1: Bounded Queue, Concurrent Cache and Sync Primitives

> The runnable companion project for this lab lives at [`code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/`](../../../../code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/). Expect roughly **8–12 hours** of hands-on work (`reading_time_minutes` counts pure reading minutes, not hands-on time).
> The runnable companion project for this lab lives at `code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/`. Expect roughly **8–12 hours** of hands-on work (`reading_time_minutes` counts pure reading minutes, not hands-on time).

## Goal

Expand Down Expand Up @@ -145,7 +145,7 @@ That lambda predicate is your lifeline. If you write `not_full_.wait(lock)` (no

> **Don't be fooled by the tests**: `test_milestone1` checks "you can push and pop, FIFO order, blocking behavior, multiple producers with no loss or duplication" — **all behavior, never whether you used a predicate wait**. You could perfectly well pass the tests with a bare `wait()` (no predicate), if no spurious wakeup happens to fire. But that's a time bomb: under high concurrency or particular schedulings, it will go off. **The real acceptance criterion: every wait in `push`/`pop` is a predicate wait (`cv.wait(lock, predicate)`); there is no bare `wait()`.** Run MS4's stress test under TSan, and a bare wait will sooner or later surface as a data race or an out-of-bounds access.

[`test/test_milestone1.cpp`](../../../../code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/test/test_milestone1.cpp) covers four scenarios: single push/pop, FIFO order, pop blocking until a push arrives, and multiple producers with no loss or duplication under concurrency.
`test/test_milestone1.cpp` covers four scenarios: single push/pop, FIFO order, pop blocking until a push arrives, and multiple producers with no loss or duplication under concurrency.

## Milestone 2: close Semantics

Expand Down
12 changes: 6 additions & 6 deletions documents/vol5-concurrency/exercises/00-thread-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ related:

# Lab 0: Thread Lifecycle Lab

> 本 Lab 配套可运行工程在 [`code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/)。动手工作量约 **4–6 小时**(`reading_time_minutes` 是纯阅读分钟数,不是动手时间)。
> 本 Lab 配套可运行工程在 `code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/`。动手工作量约 **4–6 小时**(`reading_time_minutes` 是纯阅读分钟数,不是动手时间)。

## 目标

Expand Down Expand Up @@ -66,7 +66,7 @@ templates/lab0_thread_lifecycle/
└── test_milestone1.cpp … test_milestone4.cpp
```

整个 `vol5-labs/` 目录的构建说明和 dogfooding 反馈流程见 [`vol5-labs/README.md`](../../../code/volumn_codes/vol5-labs/README.md)。先把它读一遍。
整个 `vol5-labs/` 目录的构建说明和 dogfooding 反馈流程见 `vol5-labs/README.md`。先把它读一遍。

第一次构建(需要联网,FetchContent 会拉取 Catch2 v3):

Expand Down Expand Up @@ -176,7 +176,7 @@ return 汇总

### 验证

对应测试在 [`test/test_milestone1.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone1.cpp),覆盖三个场景:扫描收集到全部文件、空目录不崩溃、总字节数正确。关键断言:
对应测试在 `test/test_milestone1.cpp`,覆盖三个场景:扫描收集到全部文件、空目录不崩溃、总字节数正确。关键断言:

```cpp
TEST_CASE("MS1: scan collects all files", "[lab0][milestone1]") {
Expand Down Expand Up @@ -223,7 +223,7 @@ Milestone 1 的手工 `join()` 有个明显问题:如果在 join 循环之前

> **别被测试骗了**:`test_milestone2` 只测 `JoiningThread` 类本身(和 `FileScanner` 解耦),**不检查 `scan()` 有没有真的用它**。所以哪怕你实现了 `JoiningThread`、测试全绿,但 `scan()` 里还是裸 `std::thread` + 手工 `join()` 循环——这个 milestone 就没真正完成。**真正的验收标准:`scan()` 里看不到手工 `join()` 循环,线程容器是 `std::vector<lab0::JoiningThread>`。**

[`test/test_milestone2.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone2.cpp) 只测 `JoiningThread` 本身(和 `FileScanner` 解耦),覆盖四个场景:作用域结束自动 join、异常路径仍 join 全部 worker、move 转移所有权、`vector` 析构 join 全部。重点看异常路径这个:
`test/test_milestone2.cpp` 只测 `JoiningThread` 本身(和 `FileScanner` 解耦),覆盖四个场景:作用域结束自动 join、异常路径仍 join 全部 worker、move 转移所有权、`vector` 析构 join 全部。重点看异常路径这个:

```cpp
TEST_CASE("MS2: exception path still joins all workers", "[lab0][milestone2]") {
Expand Down Expand Up @@ -267,7 +267,7 @@ MS1 我们把文件路径列表按值传给 worker——这其实已经是安全

### 验证

[`test/test_milestone3.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone3.cpp) 验证:非整除分片也覆盖所有文件(30 文件 / 8 worker)、素数文件数(17 文件)任何分片都不丢、move-only 类型(`unique_ptr`)能安全传入线程。比如素数那个:
`test/test_milestone3.cpp` 验证:非整除分片也覆盖所有文件(30 文件 / 8 worker)、素数文件数(17 文件)任何分片都不丢、move-only 类型(`unique_ptr`)能安全传入线程。比如素数那个:

```cpp
TEST_CASE("MS3: prime file count covered by any worker count", "[lab0][milestone3]") {
Expand Down Expand Up @@ -325,7 +325,7 @@ return total;

> **别被测试骗了**:`test_milestone4` 只验结果数值对不对(和单线程一致),**不检查统计是不是真的"每 worker 局部"**。所以哪怕测试全绿,但 `scan()` 里还在用共享 `mutex`/`atomic` 统计——你其实停在 MS1,这个 milestone 没真正完成。**真正的验收标准:`scan()` 里没有锁、没有共享 atomic,统计走 `results[worker_id]` 独立槽位 + 主线程汇总。**

[`test/test_milestone4.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab0_thread_lifecycle/test/test_milestone4.cpp) 验证:多线程扫描结果与单线程逐一扫描**完全一致**(文件数、字节数、扩展名分布三项都对),外加一个 200 文件 / 8 worker 的压力测试。关键断言:
`test/test_milestone4.cpp` 验证:多线程扫描结果与单线程逐一扫描**完全一致**(文件数、字节数、扩展名分布三项都对),外加一个 200 文件 / 8 worker 的压力测试。关键断言:

```cpp
TEST_CASE("MS4: multi-threaded stats match single-threaded baseline", "[lab0][milestone4]") {
Expand Down
4 changes: 2 additions & 2 deletions documents/vol5-concurrency/exercises/01-bounded-queue.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ related:

# Lab 1: Bounded Queue, Concurrent Cache and Sync Primitives

> 本 Lab 配套可运行工程在 [`code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/`](../../../code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/)。动手工作量约 **8–12 小时**(`reading_time_minutes` 是纯阅读分钟数,不是动手时间)。
> 本 Lab 配套可运行工程在 `code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/`。动手工作量约 **8–12 小时**(`reading_time_minutes` 是纯阅读分钟数,不是动手时间)。

## 目标

Expand Down Expand Up @@ -139,7 +139,7 @@ not_empty_.notify_one();

> **别被测试骗了**:`test_milestone1` 测的是"能放能取、FIFO、阻塞行为、多生产者不丢不重"——**全是行为,不查你用没用 predicate wait**。你完全可以用裸 `wait()`(无谓词)蒙混过测试(碰巧没触发虚假唤醒)。但那是定时炸弹:高并发或特定调度下必炸。**真正的验收标准:`push`/`pop` 的等待都是谓词 wait(`cv.wait(lock, predicate)`),没有裸 `wait()`。** 用 TSan 跑 MS4 的压力测试,裸 wait 迟早会以 data race 或越界暴露。

[`test/test_milestone1.cpp`](../../../code/volumn_codes/vol5-labs/templates/lab1_bounded_queue/test/test_milestone1.cpp) 覆盖四个场景:单 push/pop、FIFO、pop 阻塞直到 push、多生产者并发不丢不重。
`test/test_milestone1.cpp` 覆盖四个场景:单 push/pop、FIFO、pop 阻塞直到 push、多生产者并发不丢不重。

## Milestone 2: close 语义

Expand Down
10 changes: 7 additions & 3 deletions scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,11 @@ export default withDrawio(defineConfig({
...sharedBase,
srcDir: '${relSrc.replace(/\\/g, '/')}',
outDir: '${relOut.replace(/\\/g, '/')}',
ignoreDeadLinks: true,
// 根站 srcDir 只有 index/tags/bookmarks(中英),首页正文链向各卷的入口物理不存在,
// 只忽略各卷挂载前缀(含 /en 侧);根站页面之间的链接(/tags /bookmarks /en/tags...)仍被死链检查覆盖。
ignoreDeadLinks: ${JSON.stringify([
...VOLUMES.flatMap(v => [v.urlPrefix, `${v.urlPrefix}/**`, `/en${v.urlPrefix}`, `/en${v.urlPrefix}/**`]),
])},
transformPageData(pageData) { applyTagsPageData(pageData); applyArticleContributors(pageData) },
title: '现代 C++ 教程',
description: '系统化的现代 C++ 教程 — 从基础入门到领域实战',
Expand Down Expand Up @@ -760,7 +764,7 @@ async function main() {

const rootSrcDir = join(BUILD_TMP, 'root-src')
mkdirSync(rootSrcDir, { recursive: true })
for (const f of ['index.md', 'tags.md']) {
for (const f of ['index.md', 'tags.md', 'bookmarks.md']) {
const s = join(DOCUMENTS, f)
if (existsSync(s)) cpSync(s, join(rootSrcDir, f))
}
Expand All @@ -772,7 +776,7 @@ async function main() {
}
if (existsSync(join(DOCUMENTS, 'en'))) {
mkdirSync(join(rootSrcDir, 'en'), { recursive: true })
for (const f of ['index.md', 'tags.md']) {
for (const f of ['index.md', 'tags.md', 'bookmarks.md']) {
const s = join(DOCUMENTS, 'en', f)
if (existsSync(s)) cpSync(s, join(rootSrcDir, 'en', f))
}
Expand Down
2 changes: 1 addition & 1 deletion scripts/check_quality.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
REQUIRED_FM_FIELDS = {'title', 'chapter', 'order'}
RECOMMENDED_FM_FIELDS = {'description', 'tags'}

SKIP_FILENAMES = {'index.md', 'tags.md', 'README.md'}
SKIP_FILENAMES = {'index.md', 'tags.md', 'bookmarks.md', 'README.md'}
SKIP_DIR_PARTS = {'images', 'generated'}

IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.drawio'}
Expand Down
4 changes: 2 additions & 2 deletions scripts/validate_frontmatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,9 +221,9 @@ def run(self) -> bool:
"""Run validation on all markdown files in tutorial directory."""
md_files = list(self.tutorial_dir.rglob('*.md'))

# Skip index.md files and tags.md (they don't need frontmatter)
# Skip index.md files and tags.md/bookmarks.md (site tool pages, not chapter articles)
# Also skip non-article files (e.g. images/ directory)
skip_names = {'index.md', 'tags.md', 'README.md'}
skip_names = {'index.md', 'tags.md', 'bookmarks.md', 'README.md'}
skip_dir_parts = {'images'}
md_files = [
f for f in md_files
Expand Down
2 changes: 2 additions & 0 deletions site/.vitepress/config/nav.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const navZh: DefaultTheme.NavItem[] = [
{
text: '更多',
items: [
{ text: '书签', link: '/bookmarks' },
{ text: '标签索引', link: '/tags' },
{ text: '附录', link: '/appendix/' },
{ text: '路线图', link: '/roadmap/' },
Expand Down Expand Up @@ -89,6 +90,7 @@ export const navEn: DefaultTheme.NavItem[] = [
{
text: 'More',
items: [
{ text: 'Bookmarks', link: '/en/bookmarks' },
{ text: 'Tag Index', link: '/en/tags' },
{ text: 'Appendix', link: '/en/appendix/' },
{ text: 'Roadmap', link: '/en/roadmap/' },
Expand Down
Loading
Loading