Skip to content

Commit 3a997ab

Browse files
committed
实现定时等待协作式取消
1 parent c06705c commit 3a997ab

11 files changed

Lines changed: 1232 additions & 92 deletions

‎README.md‎

Lines changed: 31 additions & 11 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 and monotonic timed scheduling. Cancellation, asynchronous I/O, and detached
21-
> execution are not implemented.
20+
> with explicit and monotonic timed scheduling. Timed waits support cooperative cancellation with
21+
> `std::stop_token`; asynchronous I/O and detached execution are not implemented.
2222
2323
CMP is being built as a modern coroutine runtime and library on standard stackless C++
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.
24+
coroutines. Its explicit `co_await` model now covers scheduling, monotonic timers, and cancellable
25+
timed waits and can grow, in small verified steps, toward asynchronous I/O and safe handling of
26+
blocking 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, cancellation, or async I/O already exist.
51+
- M:N scheduling, work stealing, general cancellation propagation, 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,10 +72,10 @@ cd examples/basic
7272
mcpp run
7373
```
7474

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.
75+
The example prints `Coroutine result: 42` after a short RunLoop timer, then prints
76+
`Coroutine cancelled` from a pre-cancelled timed wait. Both messages come from `Task<void>`
77+
coroutines. It proves that an independent mcpp package can import `mcpplibs.cmp`, compose Tasks,
78+
and use timed scheduling and cancellation through the public RunLoop.
7979

8080
## Current API
8181

@@ -85,6 +85,7 @@ import mcpplibs.cmp;
8585

8686
using mcpplibs::cmp::Task;
8787
using mcpplibs::cmp::RunLoop;
88+
using mcpplibs::cmp::OperationCancelled;
8889

8990
using namespace std::chrono_literals;
9091

@@ -99,9 +100,22 @@ Task<void> print_answer(RunLoop::Scheduler scheduler) {
99100
co_return;
100101
}
101102

103+
Task<void> print_cancellation(RunLoop::Scheduler scheduler, std::stop_token token) {
104+
try {
105+
co_await scheduler.schedule_after(1s, token);
106+
} catch (const OperationCancelled&) {
107+
std::println("Coroutine cancelled");
108+
}
109+
co_return;
110+
}
111+
102112
int main() {
103113
RunLoop loop {};
104114
loop.run(print_answer(loop.get_scheduler()));
115+
116+
std::stop_source source {};
117+
source.request_stop();
118+
loop.run(print_cancellation(loop.get_scheduler(), source.get_token()));
105119
}
106120
```
107121
@@ -123,6 +137,11 @@ never resumes it inline. Scheduler handles are copyable, but remain tied to thei
123137
RunLoop. Sequential `run()` calls are supported; nested or concurrent calls are rejected. A
124138
moved-from Task must not be awaited.
125139
140+
The timed overloads accepting `std::stop_token` also always suspend. If cancellation wins before
141+
the deadline, awaiting throws `OperationCancelled`; a late stop request cannot replace an already
142+
queued deadline completion. The stop callback only wakes the RunLoop, so user coroutine code is
143+
still resumed by the thread driving `run()`. Cancellation currently performs an O(n) timer lookup.
144+
126145
RunLoop is not a background thread and does not make blocking code asynchronous. A Task that
127146
suspends without arranging a future resume can leave `run()` waiting indefinitely. CMP does not
128147
provide automatic thread affinity: after an external awaiter resumes on another thread, explicitly
@@ -171,7 +190,8 @@ Runtime work is split into independently reviewable phases:
171190
1. package identity and importable-module bootstrap — implemented;
172191
2. coroutine task and lifetime semantics — initial `Task` implemented;
173192
3. a root runner and minimal single-thread scheduler — initially implemented;
174-
4. monotonic Timer v1 — implemented; cancellation and structured wake-up paths remain;
193+
4. monotonic Timer v1 and cancellable timed waits — implemented; structured task ownership and
194+
wake-up paths remain;
175195
5. multi-worker scheduling and work stealing;
176196
6. asynchronous I/O integration and a blocking pool.
177197

‎README.zh.hant.md‎

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

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

2627
## 為什麼叫 CMP?
2728

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

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

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

7576
## 目前 API
7677

@@ -80,6 +81,7 @@ import mcpplibs.cmp;
8081

8182
using mcpplibs::cmp::Task;
8283
using mcpplibs::cmp::RunLoop;
84+
using mcpplibs::cmp::OperationCancelled;
8385

8486
using namespace std::chrono_literals;
8587

@@ -94,9 +96,22 @@ Task<void> print_answer(RunLoop::Scheduler scheduler) {
9496
co_return;
9597
}
9698

99+
Task<void> print_cancellation(RunLoop::Scheduler scheduler, std::stop_token token) {
100+
try {
101+
co_await scheduler.schedule_after(1s, token);
102+
} catch (const OperationCancelled&) {
103+
std::println("Coroutine cancelled");
104+
}
105+
co_return;
106+
}
107+
97108
int main() {
98109
RunLoop loop {};
99110
loop.run(print_answer(loop.get_scheduler()));
111+
112+
std::stop_source source {};
113+
source.request_stop();
114+
loop.run(print_cancellation(loop.get_scheduler(), source.get_token()));
100115
}
101116
```
102117
@@ -114,6 +129,10 @@ continuation 之間直接轉移,並透過 RAII 銷毀未消費的協程框架
114129
任務具備執行資格,不會 inline 恢復。Scheduler 可以複製,但始終屬於建立它的 RunLoop。
115130
支援依序多次呼叫 `run()`,巢狀或並行呼叫會被拒絕。已經被移動的 Task 不得再次等待。
116131
132+
接受 `std::stop_token` 的定時多載也始終暫停。如果取消先於期限獲勝,等待會拋出
133+
`OperationCancelled`;較晚的停止要求不能取代已經進入就緒佇列的期限完成。停止回呼只喚醒
134+
RunLoop,使用者協程仍由執行 `run()` 的執行緒恢復。目前取消透過 O(n) 掃描定位計時器。
135+
117136
RunLoop 不是背景執行緒,也不會把阻塞程式碼自動變成非同步程式碼。如果 Task 暫停後沒有
118137
安排未來的恢復動作,`run()` 可能一直等待。CMP 不提供隱式執行緒親和:外部 awaiter 在其他
119138
執行緒恢復協程後,需要明確等待目標 Scheduler 才會返回對應 RunLoop。
@@ -159,7 +178,7 @@ CMP 目前不追蹤 `mcpp.lock`,`.gitignore` 明確執行這項儲存庫約定
159178
1. 套件識別與可匯入模組 bootstrap——已完成;
160179
2. 協程 task 與生命週期語意——已實作初始 `Task`;
161180
3. 根任務驅動器和最小單執行緒排程器——已完成初始實作;
162-
4. 單調時鐘 Timer v1——已實作;取消和結構化喚醒路徑仍待開發;
181+
4. 單調時鐘 Timer v1 和可取消定時等待——已實作;結構化任務所有權和喚醒路徑仍待開發;
163182
5. 多 worker 排程與 work stealing;
164183
6. 非同步 I/O 整合和 blocking pool。
165184

‎README.zh.md‎

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

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

2527
## 为什么叫 CMP?
2628

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

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

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

7476
## 当前 API
7577

@@ -79,6 +81,7 @@ import mcpplibs.cmp;
7981

8082
using mcpplibs::cmp::Task;
8183
using mcpplibs::cmp::RunLoop;
84+
using mcpplibs::cmp::OperationCancelled;
8285

8386
using namespace std::chrono_literals;
8487

@@ -93,9 +96,22 @@ Task<void> print_answer(RunLoop::Scheduler scheduler) {
9396
co_return;
9497
}
9598

99+
Task<void> print_cancellation(RunLoop::Scheduler scheduler, std::stop_token token) {
100+
try {
101+
co_await scheduler.schedule_after(1s, token);
102+
} catch (const OperationCancelled&) {
103+
std::println("Coroutine cancelled");
104+
}
105+
co_return;
106+
}
107+
96108
int main() {
97109
RunLoop loop {};
98110
loop.run(print_answer(loop.get_scheduler()));
111+
112+
std::stop_source source {};
113+
source.request_stop();
114+
loop.run(print_cancellation(loop.get_scheduler(), source.get_token()));
99115
}
100116
```
101117
@@ -113,6 +129,10 @@ continuation 之间直接转移,并通过 RAII 销毁未消费的协程帧。
113129
任务具备运行资格,不会 inline 恢复。Scheduler 可以复制,但始终属于创建它的 RunLoop。
114130
支持顺序多次调用 `run()`,嵌套或并发调用会被拒绝。已经被移动的 Task 不得再次等待。
115131
132+
接受 `std::stop_token` 的定时重载也始终挂起。如果取消先于期限获胜,等待会抛出
133+
`OperationCancelled`;较晚的停止请求不能替换已经进入就绪队列的期限完成。停止回调只唤醒
134+
RunLoop,用户协程仍由执行 `run()` 的线程恢复。当前取消通过 O(n) 扫描定位定时器。
135+
116136
RunLoop 不是后台线程,也不会把阻塞代码自动变成异步代码。如果 Task 挂起后没有安排未来的
117137
恢复动作,`run()` 可能一直等待。CMP 不提供隐式线程亲和:外部 awaiter 在其他线程恢复协程
118138
后,需要显式等待目标 Scheduler 才会返回对应 RunLoop。
@@ -158,7 +178,7 @@ CMP 当前不跟踪 `mcpp.lock`,`.gitignore` 明确执行这一仓库约定。
158178
1. 包身份和可导入模块 bootstrap——已完成;
159179
2. 协程 task 与生命周期语义——已实现初始 `Task`;
160180
3. 根任务驱动器和最小单线程调度器——已完成初始实现;
161-
4. 单调时钟 Timer v1——已实现;取消和结构化唤醒路径仍待开发;
181+
4. 单调时钟 Timer v1 和可取消定时等待——已实现;结构化任务所有权和唤醒路径仍待开发;
162182
5. 多 worker 调度与 work stealing;
163183
6. 异步 I/O 集成和 blocking pool。
164184

0 commit comments

Comments
 (0)