Skip to content

Commit c06705c

Browse files
实现 RunLoop 定时调度 (#3)
1 parent e729782 commit c06705c

12 files changed

Lines changed: 946 additions & 97 deletions

File tree

‎.agent/HANDOFF.md‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# CMP 当前工作状态
2+
3+
## 项目概览
4+
5+
CMP 是使用 mcpp 构建的 C++23 Modules 协程运行时库。当前公开核心包括惰性、唯一所有权的
6+
`Task<T>`,单消费者 `RunLoop`,以及可复制的 `RunLoop::Scheduler`。Scheduler 支持普通
7+
`schedule()`、相对期限 `schedule_after()` 和绝对期限 `schedule_at()`;所有 continuation
8+
均由调用 `RunLoop::run()` 的线程恢复。
9+
10+
项目使用 `mcpplibs.cmp` 模块;`.xlings.json` 固定 mcpp 2026.8.11.2,`mcpp.toml` 固定
11+
LLVM 22.1.8,测试依赖为 `compat.gtest` 1.15.2。实现位于 `src/`,契约测试位于 `tests/`,
12+
`examples/basic` 是独立的 path-dependency 消费示例。
13+
14+
## 当前目标与状态
15+
16+
Timer v1 已实现并完成本地 Dev、Release 和重复并发验证。本次交付提交位于
17+
`feature/timer-v1`,并推送至 `origin` 的同名分支。用户没有授权创建或修改 PR;Linux、
18+
macOS 和 Windows CI 结果仍待确认。
19+
20+
## 已完成工作
21+
22+
- 为 Scheduler 导出 `Clock`、`Duration`、`TimePoint`,新增 `schedule_after()` 与
23+
`schedule_at()`。
24+
- 在 `RunLoopState` 中加入标准库最小 Timer 堆、溢出安全的相对期限计算和新最早期限通知。
25+
- `drive()` 使用非谓词 `wait_until()`,每轮最多把一个到期 Timer 追加到现有 FIFO,并始终
26+
在互斥量外恢复协程。
27+
- 已到期和非正期限仍会挂起并直接进入 FIFO;最大时长不会回绕成立即任务。
28+
- Timer 加入现有 outstanding-work、失败清理和 RunLoop 顺序复用契约。
29+
- 新增正常期限、立即期限、同期限、跨线程唤醒、无效 Scheduler、溢出清理和十万次立即
30+
Timer 测试。
31+
- 独立示例会在协程中等待 `10ms` 后打印;英文、简体中文和繁体中文 README/架构文档已同步。
32+
33+
## 重要决策
34+
35+
- 固定使用 `std::chrono::steady_clock`;相对期限在 `await_suspend()` 时测量。
36+
- 正延迟先比较 `TimePoint::max() - delay`,确认安全后才执行加法。
37+
- 所有 timed await 都真正挂起;期限到达只表示具备运行资格,不承诺精确恢复时间。
38+
- 未到期 Timer 使用 `std::priority_queue`,不增加后台线程、平台 Timer、依赖或公开 Timer
39+
对象。
40+
- 仅新最早期限需要打断既有 timed wait;等待前复制堆顶期限,避免解锁后堆重排导致引用
41+
失效。
42+
- 每轮最多提升一个到期 Timer,限制持锁时间,并通过追加到 FIFO 平衡 ready/timer 公平性。
43+
- Timer 仍是非所有权调度引用;取消和 exactly-once 竞态留到独立设计。
44+
- `std::priority_queue` 无法高效删除任意 Timer;取消阶段必须选择惰性失效或更换数据结构。
45+
46+
## 修改 / 重要文件
47+
48+
- `src/run_loop.cppm`:Timer 堆、驱动循环和公开 timed Scheduler API。
49+
- `tests/run_loop_test.cpp`:Timer v1 契约与边界测试。
50+
- `examples/basic/src/main.cpp`:协程内定时打印示例。
51+
- `README.md`、`README.zh.md`、`README.zh.hant.md`:三语公开说明。
52+
- `docs/architecture.md`、`docs/architecture.zh.md`、`docs/architecture.zh.hant.md`:三语架构契约。
53+
- `docs/superpowers/specs/2026-08-12-cmp-timer-v1-design.md`:已实现设计。
54+
- `docs/superpowers/plans/2026-08-12-cmp-timer-v1.md`:已完成实施计划和本地结果。
55+
56+
## 验证情况
57+
58+
- 基线:修改实现前 22 项测试通过。
59+
- 红灯:新增测试因缺少 timed API 按预期编译失败。
60+
- Dev:`mcpp build` 通过,29/29 测试通过。
61+
- Release:`mcpp build` 通过,29/29 测试通过。
62+
- 稳定性:Release RunLoop 的 21 项测试额外连续运行 10 次,全部通过。
63+
- 示例:`examples/basic` 构建运行通过,输出 `Coroutine result: 42`。
64+
- mcpp 每次运行都会提示现有 SubOS 缺少 `subos_info`;不影响本次编译、测试或示例结果。
65+
- 本次 Git 授权仅包括提交和推送;没有创建或修改 PR,也没有执行其他 GitHub 写操作。
66+
- 三平台 CI 尚未在当前工作状态中确认。
67+
68+
## 已知问题 / 风险
69+
70+
- 只有当前 Linux WSL2 环境完成本地验证,macOS/Windows 结果必须以 CI 为准。
71+
- 并发测试使用真实 `steady_clock` 和宽松存活上界;目前重复测试稳定,但 CI 仍需确认。
72+
- Timer v1 不支持取消;外部销毁已经发布的协程帧仍由所有者负责。
73+
74+
## 剩余工作
75+
76+
1. 后续获得相应授权后,确认 Linux、macOS 和 Windows CI 结果。
77+
2. CI 通过后,只有用户明确授权才可创建或修改 PR。
78+
3. Timer v1 合并后,再单独设计取消与结构化唤醒路径。
79+
80+
## 推荐下一步
81+
82+
先确认三平台 CI;若全部通过,再由用户决定是否授权 PR。不要从本次提交和推送授权推导 PR
83+
或合并权限。

‎README.md‎

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717

1818
> [!IMPORTANT]
1919
> CMP provides a lazy, single-consumer `Task<T>` / `Task<void>` and a caller-thread `RunLoop`
20-
> with explicit scheduling. Cancellation, timers, asynchronous I/O, and detached execution are
21-
> not implemented.
20+
> with explicit and monotonic timed scheduling. Cancellation, asynchronous I/O, and detached
21+
> execution are not implemented.
2222
2323
CMP is being built as a modern coroutine runtime and library on standard stackless C++
24-
coroutines. The intended direction is an explicit `co_await` model that can grow, in small
25-
verified steps, toward scheduling, timers, asynchronous I/O, cancellation, and safe handling of
26-
blocking work.
24+
coroutines. Its explicit `co_await` model now covers scheduling and monotonic timers and can grow,
25+
in small verified steps, toward asynchronous I/O, cancellation, and safe handling of blocking
26+
work.
2727

2828
## Why CMP?
2929

@@ -48,7 +48,7 @@ promise that:
4848
- a task is automatically equivalent to a Go goroutine;
4949
- an arbitrary blocking call becomes non-blocking;
5050
- coroutine switching is safe directly inside a signal handler;
51-
- M:N scheduling, work stealing, timers, cancellation, or async I/O already exist.
51+
- M:N scheduling, work stealing, cancellation, or async I/O already exist.
5252

5353
Those capabilities must be designed and verified individually. The expected direction is
5454
explicit async I/O awaiters, a dedicated blocking pool, and cooperative safe points.
@@ -72,9 +72,10 @@ cd examples/basic
7272
mcpp run
7373
```
7474

75-
The example prints `Coroutine result: 42` from inside a scheduled `Task<void>` coroutine and exits
76-
successfully. It proves that an independent mcpp package can resolve the path dependency, import
77-
`mcpplibs.cmp`, compose Tasks, and drive them through the public RunLoop.
75+
The example waits for a short RunLoop timer, prints `Coroutine result: 42` from inside a
76+
`Task<void>` coroutine, and exits successfully. It proves that an independent mcpp package can
77+
resolve the path dependency, import `mcpplibs.cmp`, compose Tasks, and use timed scheduling through
78+
the public RunLoop.
7879

7980
## Current API
8081

@@ -85,12 +86,14 @@ import mcpplibs.cmp;
8586
using mcpplibs::cmp::Task;
8687
using mcpplibs::cmp::RunLoop;
8788

89+
using namespace std::chrono_literals;
90+
8891
Task<int> answer() {
8992
co_return 42;
9093
}
9194

9295
Task<void> print_answer(RunLoop::Scheduler scheduler) {
93-
co_await scheduler.schedule();
96+
co_await scheduler.schedule_after(10ms);
9497
auto value = co_await answer();
9598
std::println("Coroutine result: {}", value);
9699
co_return;
@@ -114,9 +117,11 @@ library.
114117
115118
`RunLoop::run()` consumes one root Task, executes ready coroutines on the calling thread, returns
116119
its value, and rethrows its exception. `Scheduler::schedule()` always suspends and queues the
117-
continuation. Scheduler handles are copyable, but remain tied to their originating RunLoop.
118-
Sequential `run()` calls are supported; nested or concurrent calls are rejected. A moved-from Task
119-
must not be awaited.
120+
continuation. `schedule_after()` waits for a relative `steady_clock` duration, while
121+
`schedule_at()` waits for an absolute steady-clock time point; expiry makes work eligible and
122+
never resumes it inline. Scheduler handles are copyable, but remain tied to their originating
123+
RunLoop. Sequential `run()` calls are supported; nested or concurrent calls are rejected. A
124+
moved-from Task must not be awaited.
120125
121126
RunLoop is not a background thread and does not make blocking code asynchronous. A Task that
122127
suspends without arranging a future resume can leave `run()` waiting indefinitely. CMP does not
@@ -166,7 +171,7 @@ Runtime work is split into independently reviewable phases:
166171
1. package identity and importable-module bootstrap — implemented;
167172
2. coroutine task and lifetime semantics — initial `Task` implemented;
168173
3. a root runner and minimal single-thread scheduler — initially implemented;
169-
4. timers, cancellation, and structured wake-up paths;
174+
4. monotonic Timer v1 — implemented; cancellation and structured wake-up paths remain;
170175
5. multi-worker scheduling and work stealing;
171176
6. asynchronous I/O integration and a blocking pool.
172177

‎README.zh.hant.md‎

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@
1717

1818
> [!IMPORTANT]
1919
> CMP 已提供延遲啟動、單一消費者的 `Task<T>` / `Task<void>`,以及在呼叫執行緒運行、
20-
> 支援明確排程的 `RunLoop`。取消、計時器、非同步 I/O 和 detached 執行尚未實作。
20+
> 支援明確排程和單調時鐘定時排程的 `RunLoop`。取消、非同步 I/O 和 detached 執行尚未實作。
2121
22-
CMP 計畫以標準無堆疊 C++ 協程建構現代協程執行期與函式庫。專案將以明確的 `co_await`
23-
為主線,透過經過驗證的小步驟逐步探索排程、計時器、非同步 I/O、取消,以及阻塞工作的安全隔離。
22+
CMP 計畫以標準無堆疊 C++ 協程建構現代協程執行期與函式庫。明確的 `co_await` 模型現已
23+
涵蓋排程和單調時鐘計時器,並將透過經過驗證的小步驟繼續探索非同步 I/O、取消,以及阻塞
24+
工作的安全隔離。
2425

2526
## 為什麼叫 CMP?
2627

@@ -43,7 +44,7 @@ C++ 標準協程是語言機制,不是完整執行期。因此 CMP 不會宣
4344
- task 自動等同於 Go goroutine;
4445
- 任意阻塞呼叫會自動成為非阻塞呼叫;
4546
- 可以直接在訊號處理器中安全切換協程;
46-
- M:N 排程、work stealing、計時器、取消或非同步 I/O 已經實作。
47+
- M:N 排程、work stealing、取消或非同步 I/O 已經實作。
4748

4849
這些能力必須分別設計和驗證。預期方向是明確的非同步 I/O awaiter、專用 blocking pool
4950
以及協作式安全點。
@@ -67,9 +68,9 @@ cd examples/basic
6768
mcpp run
6869
```
6970

70-
範例會從經過排程的 `Task<void>` 協程內部印出 `Coroutine result: 42`,然後以成功狀態結束。
71-
它負責證明獨立 mcpp 套件能夠解析路徑相依、匯入 `mcpplibs.cmp`、組合 Task 並透過公開
72-
RunLoop 驅動任務。
71+
範例會先等待一個短 RunLoop 計時器,再從 `Task<void>` 協程內部印出
72+
`Coroutine result: 42`,然後以成功狀態結束。它負責證明獨立 mcpp 套件能夠解析路徑相依、
73+
匯入 `mcpplibs.cmp`、組合 Task 並透過公開 RunLoop 使用定時排程。
7374

7475
## 目前 API
7576

@@ -80,12 +81,14 @@ import mcpplibs.cmp;
8081
using mcpplibs::cmp::Task;
8182
using mcpplibs::cmp::RunLoop;
8283

84+
using namespace std::chrono_literals;
85+
8386
Task<int> answer() {
8487
co_return 42;
8588
}
8689

8790
Task<void> print_answer(RunLoop::Scheduler scheduler) {
88-
co_await scheduler.schedule();
91+
co_await scheduler.schedule_after(10ms);
8992
auto value = co_await answer();
9093
std::println("Coroutine result: {}", value);
9194
co_return;
@@ -106,9 +109,10 @@ continuation 之間直接轉移,並透過 RAII 銷毀未消費的協程框架
106109
`std`,不會向使用端重新匯出整個標準函式庫。
107110
108111
`RunLoop::run()` 消費一個根 Task,在呼叫執行緒執行就緒協程,回傳結果並重新拋出例外。
109-
`Scheduler::schedule()` 始終暫停目前協程並把 continuation 放入佇列。Scheduler 可以複製,
110-
但始終屬於建立它的 RunLoop。支援依序多次呼叫 `run()`,巢狀或並行呼叫會被拒絕。已經被
111-
移動的 Task 不得再次等待。
112+
`Scheduler::schedule()` 始終暫停目前協程並把 continuation 放入佇列;`schedule_after()`
113+
等待相對的 `steady_clock` 時長,`schedule_at()` 等待絕對的單調時鐘時間點。期限到達只讓
114+
任務具備執行資格,不會 inline 恢復。Scheduler 可以複製,但始終屬於建立它的 RunLoop。
115+
支援依序多次呼叫 `run()`,巢狀或並行呼叫會被拒絕。已經被移動的 Task 不得再次等待。
112116
113117
RunLoop 不是背景執行緒,也不會把阻塞程式碼自動變成非同步程式碼。如果 Task 暫停後沒有
114118
安排未來的恢復動作,`run()` 可能一直等待。CMP 不提供隱式執行緒親和:外部 awaiter 在其他
@@ -155,7 +159,7 @@ CMP 目前不追蹤 `mcpp.lock`,`.gitignore` 明確執行這項儲存庫約定
155159
1. 套件識別與可匯入模組 bootstrap——已完成;
156160
2. 協程 task 與生命週期語意——已實作初始 `Task`;
157161
3. 根任務驅動器和最小單執行緒排程器——已完成初始實作;
158-
4. 計時器、取消和結構化喚醒路徑;
162+
4. 單調時鐘 Timer v1——已實作;取消和結構化喚醒路徑仍待開發;
159163
5. 多 worker 排程與 work stealing;
160164
6. 非同步 I/O 整合和 blocking pool。
161165

‎README.zh.md‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717

1818
> [!IMPORTANT]
1919
> CMP 已提供懒启动、单消费者的 `Task<T>` / `Task<void>`,以及在调用线程运行、支持显式
20-
> 调度的 `RunLoop`。取消、定时器、异步 I/O 和 detached 执行尚未实现。
20+
> 调度和单调时钟定时调度的 `RunLoop`。取消、异步 I/O 和 detached 执行尚未实现。
2121
22-
CMP 计划基于标准无栈 C++ 协程构建现代协程运行时和库。项目将以显式 `co_await` 为主线,
23-
通过经过验证的小步骤逐步探索调度、定时器、异步 I/O、取消以及阻塞工作的安全隔离。
22+
CMP 计划基于标准无栈 C++ 协程构建现代协程运行时和库。显式 `co_await` 模型现已覆盖调度和
23+
单调时钟定时器,并将通过经过验证的小步骤继续探索异步 I/O、取消以及阻塞工作的安全隔离。
2424

2525
## 为什么叫 CMP?
2626

@@ -43,7 +43,7 @@ C++ 标准协程是语言机制,不是完整运行时。因此 CMP 不会宣
4343
- task 自动等同于 Go goroutine;
4444
- 任意阻塞调用会自动变成非阻塞调用;
4545
- 可以直接在信号处理器中安全切换协程;
46-
- M:N 调度、work stealing、定时器、取消或异步 I/O 已经实现。
46+
- M:N 调度、work stealing、取消或异步 I/O 已经实现。
4747

4848
这些能力必须分别设计和验证。预期方向是显式异步 I/O awaiter、专用 blocking pool
4949
以及协作式安全点。
@@ -67,9 +67,9 @@ cd examples/basic
6767
mcpp run
6868
```
6969

70-
示例会从经过调度的 `Task<void>` 协程内部打印 `Coroutine result: 42`,然后以成功状态退出。
71-
它负责证明独立 mcpp 包能够解析路径依赖、导入 `mcpplibs.cmp`、组合 Task 并通过公共
72-
RunLoop 驱动任务。
70+
示例会先等待一个短 RunLoop 定时器,再从 `Task<void>` 协程内部打印
71+
`Coroutine result: 42`,然后以成功状态退出。它负责证明独立 mcpp 包能够解析路径依赖、
72+
导入 `mcpplibs.cmp`、组合 Task 并通过公共 RunLoop 使用定时调度。
7373

7474
## 当前 API
7575

@@ -80,12 +80,14 @@ import mcpplibs.cmp;
8080
using mcpplibs::cmp::Task;
8181
using mcpplibs::cmp::RunLoop;
8282

83+
using namespace std::chrono_literals;
84+
8385
Task<int> answer() {
8486
co_return 42;
8587
}
8688

8789
Task<void> print_answer(RunLoop::Scheduler scheduler) {
88-
co_await scheduler.schedule();
90+
co_await scheduler.schedule_after(10ms);
8991
auto value = co_await answer();
9092
std::println("Coroutine result: {}", value);
9193
co_return;
@@ -106,9 +108,10 @@ continuation 之间直接转移,并通过 RAII 销毁未消费的协程帧。
106108
`std`,不会向使用方重新导出整个标准库。
107109
108110
`RunLoop::run()` 消费一个根 Task,在调用线程执行就绪协程,返回结果并重新抛出异常。
109-
`Scheduler::schedule()` 始终挂起当前协程并把 continuation 放入队列。Scheduler 可以复制,
110-
但始终属于创建它的 RunLoop。支持顺序多次调用 `run()`,嵌套或并发调用会被拒绝。已经被
111-
移动的 Task 不得再次等待。
111+
`Scheduler::schedule()` 始终挂起当前协程并把 continuation 放入队列;`schedule_after()`
112+
等待相对的 `steady_clock` 时长,`schedule_at()` 等待绝对的单调时钟时间点。期限到达只让
113+
任务具备运行资格,不会 inline 恢复。Scheduler 可以复制,但始终属于创建它的 RunLoop。
114+
支持顺序多次调用 `run()`,嵌套或并发调用会被拒绝。已经被移动的 Task 不得再次等待。
112115
113116
RunLoop 不是后台线程,也不会把阻塞代码自动变成异步代码。如果 Task 挂起后没有安排未来的
114117
恢复动作,`run()` 可能一直等待。CMP 不提供隐式线程亲和:外部 awaiter 在其他线程恢复协程
@@ -155,7 +158,7 @@ CMP 当前不跟踪 `mcpp.lock`,`.gitignore` 明确执行这一仓库约定。
155158
1. 包身份和可导入模块 bootstrap——已完成;
156159
2. 协程 task 与生命周期语义——已实现初始 `Task`;
157160
3. 根任务驱动器和最小单线程调度器——已完成初始实现;
158-
4. 定时器、取消和结构化唤醒路径;
161+
4. 单调时钟 Timer v1——已实现;取消和结构化唤醒路径仍待开发;
159162
5. 多 worker 调度与 work stealing;
160163
6. 异步 I/O 集成和 blocking pool。
161164

0 commit comments

Comments
 (0)