Skip to content

Latest commit

 

History

History
167 lines (122 loc) · 11.4 KB

File metadata and controls

167 lines (122 loc) · 11.4 KB

CMP 架構

English · 简体中文 · 繁體中文

本文說明目前 API 的執行與生命週期契約。安裝及完整最小程式見 README;歷史驗證和待完成事項見實作記錄(簡體中文)。

與 Go GMP 的機制對照、技術邊界和待辦見 GMP 對照文件(簡體中文)。

執行模型

元件 職責與執行位置
Task<T> 持有協程框架與結果;延遲啟動、單一消費者,本身不建立執行緒
RunLoop run(task) 阻塞呼叫執行緒並在該執行緒消費排程佇列,回傳根任務的值或例外
ThreadPool 固定數量的 worker 依共用 FIFO 領取工作;不保證完成順序
IoContext 一個私有 I/O driver 驅動原生 TCP,多條連線可共用同一 context
組合與同步原語 持有子任務或註冊等待者;恢復執行緒由最後完成的子任務、通知者或解鎖者決定

執行緒親和不自動傳遞:任務從其他執行緒恢復後,透過 co_await caller.schedule() 明確返回目標執行器。阻塞程式碼會阻塞目前執行緒;沒有恢復來源的暫停任務會使 run() 一直等待。

以下函式片段共用這些宣告;呼叫時使用 README 中的 RunLoop::run():

import std;
import mcpplibs.cmp;

namespace cmp = mcpplibs::cmp;

任務與結構化並行

Task<T> 支援值和 void,不支援參考或陣列結果。它可以移動建構,不能複製或移動賦值;co_await std::move(task) 消費具名任務,重複消費會終止處理程序。未消費 Task 銷毀自己的框架;已啟動任務必須保留到所有恢復來源結束,不能用銷毀 Task 代替取消。

when_all() 等待時依輸入順序啟動所有任務,先等待全部結束,再依輸入順序回傳結果或拋出第一個例外,不會因一個子任務失敗而自動取消其他任務。可變參數形式回傳 tuple,void 對應 std::monostate;vector 形式回傳相同索引順序的結果。父任務在最後完成的子任務執行緒繼續。

下面同時等待兩個計時任務;loop.run(total(loop.get_scheduler())) 回傳 42:

cmp::Task<int> later(cmp::RunLoop::Scheduler scheduler, int value) {
    co_await scheduler.schedule_after(std::chrono::milliseconds { 1 });
    co_return value;
}

cmp::Task<int> total(cmp::RunLoop::Scheduler scheduler) {
    auto [first, second] = co_await cmp::when_all(
        later(scheduler, 20), later(scheduler, 22));
    co_return first + second;
}

TaskGroup 用於逐步接納 Task<void>:

  • spawn() 在返回前啟動子任務,執行到第一次暫停;深層遞迴 spawn 應在子任務開頭明確 schedule()。
  • join() 延遲啟動且只能等待一次。join 期間活躍子任務仍可 spawn;join 已開始且活動數歸零時永久關閉接納。join 前暫時為空的 group 仍可接納。
  • join 等待全部子任務結束,依接納順序選擇首個例外並釋放終態包裝框架。join 前的保留量隨累計任務數增加,可用分批 group 控制。
  • 解構時必須未使用或已完成 join,否則終止處理程序。接納或作用域主體可能拋出例外時,在首次 spawn 前建立 join Task,捕捉錯誤、要求停止,再等待 join 後處理錯誤;可執行範例見結構化清理測試。
  • get_stop_token() 必須明確傳給子任務。cancel_and_join() 在被等待時要求停止並 join,不會搶佔子任務。

事件、鎖和借用資料必須涵蓋全部等待者的生命週期。暫存捕捉型協程 lambda 的閉包可能先於延遲啟動的協程銷毀,優先使用參數按值傳入的具名協程函式。

排程與取消

API 契約
RunLoop::Scheduler schedule() 始終排隊;schedule_after() / schedule_at() 使用 steady_clock,已到期或非正延遲也排隊,不保證精確喚醒時刻
ThreadPool::Scheduler schedule() 始終排隊,在任意 worker 恢復;不提供計時排程
schedule(token) 及計時多載 預取消也排隊;取消與消費選擇一個結果,取消獲勝拋出 OperationCancelled,晚到的 stop 不取代已選結果

Scheduler 是可複製的弱身分控制代碼,不延長執行器生命週期。RunLoop 必須處於有效的 run() 中;同一個 RunLoop 只允許依序重用,巢狀或並行 run 拋出 std::logic_error,根任務結束時仍有遺留佇列工作也會失敗。

ThreadPool 解構關閉接納、排空已接納工作並 join worker;不得從自己的 worker 解構。它擁有執行緒,不擁有上層 Task。明確傳入零個 worker 會拋出 std::invalid_argument。

取消需要明確傳遞 token,各 API 的優先順序分別定義。停止要求不等於任務結束,釋放資源前仍要等待任務收尾。

同步原語

原語 等待與通知規則
OneShotEvent co_await event;首次 set() 永久設為已通知,在 setter 執行緒、set 返回前恢復已註冊等待者,順序未指定;不支援 reset 或取消
AsyncManualResetEvent wait(token) / co_await event;set 依 FIFO 恢復 pending 等待者,reset 只影響未來等待;預取消優先,set/cancel 只選一個結果,在獲勝的通知執行緒恢復
AsyncMutex auto guard = co_await mutex.lock_async();無競爭時 inline 繼續,否則 FIFO 交接,在 Guard 釋放執行緒恢復;無 try-lock、手動 unlock 或取消

三者不可移動。事件有 pending 等待者,或 mutex 仍被持有/有人排隊時解構,會終止處理程序。OneShotEvent 的直接巢狀 set() 鏈會增加呼叫堆疊;通知下一事件前明確 schedule() 可形成非同步邊界。

阻塞呼叫

為阻塞工作使用獨立 ThreadPool,避免佔滿 CPU worker:

cmp::Task<int> offload(
    cmp::ThreadPool::Scheduler blockingWorkers,
    cmp::RunLoop::Scheduler caller) {
    co_return co_await cmp::run_blocking(blockingWorkers, caller, [] {
        std::this_thread::sleep_for(std::chrono::milliseconds { 1 });
        return 42;
    });
}

呼叫端建立 cmp::ThreadPool blockingWorkers { 2 },再用 loop.run(offload(blockingWorkers.get_scheduler(), loop.get_scheduler())),結果為 42。run_blocking() 持有 callable,worker 領取後執行一次;排隊取消可跳過執行,開始後不能搶佔。結果透過不可取消的返回排程交付;返回 Scheduler 失敗時,其例外覆蓋業務結果或例外。

TCP

TcpStream::connect() 和 TcpListener::bind() 僅接受數值 IPv4/IPv6。下面連線至本機服務、寫完請求並執行一次讀取;read_some() 不保證讀到完整回應,訊息邊界由上層協定處理:

cmp::Task<std::size_t> send_and_read_some(
    cmp::IoContext& io,
    cmp::RunLoop::Scheduler caller,
    std::uint16_t port,
    std::span<const std::byte> request,
    std::span<std::byte> reply,
    std::stop_token token = {}) {
    auto stream = co_await cmp::TcpStream::connect(
        io, caller, "127.0.0.1", port, token);
    co_await stream.write_all(caller, request, token);
    co_return co_await stream.read_some(caller, reply, token);
}

伺服器透過 co_await cmp::TcpListener::bind(io, caller, "127.0.0.1", 0) 繫結臨時連接埠,用 local_port() 取得連接埠,再 co_await listener.accept(caller)。完整迴路用法見 TCP 測試。

  • stream/listener 只能移動建構。每個 stream 同時允許一個 read 和一個 write;每個 listener 允許一個 accept。方向佔用持續到結果交付,同方向重疊拋出 std::logic_error。
  • read/write 的 span 借用底層儲存直到 Task 完成。非空讀取回傳零表示 EOF;空緩衝區回傳零不檢測 EOF。EOF 在後續讀取保持有效,write 方向仍可用。
  • 讀取優先順序為:資源與方向校驗 → 快取 EOF/錯誤 → 預取消 → 空緩衝區 → 原生讀取。位元組與 EOF 或非取消導致的原生錯誤同時到達時,先交付位元組,下次讀取交付終態;快取的非 EOF 錯誤只交付一次。
  • write_all() 全寫或報錯,報錯不表示沒有位元組送出。預取消不啟動 I/O、不關閉仍可用的 stream;已發起寫入的取消會關閉 stream。
  • close() 執行緒安全、冪等、非阻塞;關閉獲勝時已接納操作以 OperationCancelled 收尾。取消 accept 保持 listener 可用;關閉 listener 不關閉已接受的 stream。
  • 成功、錯誤和取消都經過明確的 return Scheduler;返回排程失敗會覆蓋業務結果。Task 建立或排程接納仍可能拋出分配/建構例外。
  • IoContext 關閉資源並排空 native handler 後回收 driver,不可從自己的 driver 解構。呼叫端仍需 join 上層任務,並讓返回執行器保持可用。

實作入口

根模組為 mcpplibs.cmp,公開命名空間為 mcpplibs::cmp;CMP 不重新匯出 std 或 Asio 型別。

子系統 原始碼
框架與取消例外 task.cppm、cancellation.cppm
排程與阻塞隔離 run_loop.cppm、thread_pool.cppm、blocking.cppm
結構化並行 when_all.cppm、task_group.cppm
同步 one_shot_event.cppm、async_manual_reset_event.cppm、async_mutex.cppm
原生 TCP tcp.cppm

RunLoop 使用 awaiter 內節點組成的侵入式 FIFO 與帶索引的 timer 最小堆積;就緒入列/到期轉移不分配,就緒取消為 O(1)、timer 調整為 O(log n),未來 timer 接納可能分配。when_all 以 thread-local 啟動佇列和對稱完成轉移控制深層 join 的堆疊使用。TCP 關閉 native handle 後保留 socket 物件至已發起操作的 handler 返回。

建置與驗證

mcpp.toml 宣告 C++23、私有 TCP 相依 Asio 1.38.1,以及測試相依 gtest 1.15.2。目前不追蹤 mcpp.lock;版本固定不能取代完整供應鏈鎖定。

範圍 Linux macOS / Windows
根程式庫與測試 LLVM 22.1.8 LLVM 22.1.8
basic 與兩個 benchmark consumer GCC 16.1.0 LLVM 22.1.8;readiness 負載僅適用 POSIX

在儲存庫根目錄用 Bash 執行,Windows 使用 Git Bash:

scripts/qualify-command.sh mcpp build --profile dev --strict --cache=off
scripts/qualify-command.sh mcpp test --profile dev --strict --cache=off
scripts/qualify-command.sh mcpp build --profile release --strict --cache=off
scripts/qualify-command.sh mcpp test --profile release --strict --cache=off
cd examples/basic
../../scripts/qualify-command.sh mcpp build --profile dev --strict --cache=off
../../scripts/qualify-command.sh mcpp run
../../scripts/qualify-command.sh mcpp build --profile release --strict --cache=off

命令檢查拒絕非零退出和 warning/error 診斷;CI依平台執行。測試清單以 mcpp test --list 為準,Linux 通過不代表其他平台已驗收。效能樣本、Sanitizer 與平台驗證缺口單獨記錄於實作報告,不作為穩定 API 保證。