訊息傳遞框架與 ATM 範例 (ATM Message-Passing Example)
Overview Table
| 項目 | 要點 |
|---|---|
| 框架類別 | message_base / wrapped_message<Msg> / queue / sender / receiver / dispatcher / TemplateDispatcher<> |
| 型別判別 | dynamic_cast<wrapped_message<Msg>*> 對指向基底類別的指標做執行期匹配 |
| dispatch 鏈 | wait().handle<M1>(f1).handle<M2>(f2)...;真正的等待發生在解構子 |
| 結束機制 | 送 close_queue 訊息 → dispatcher::dispatch() 擲出例外 → run() 的 catch 收尾 |
| 三個角色 | atm(狀態機)、bank_machine(帳務)、interface_machine(顯示/出鈔),各跑一條執行緒 |
| 狀態機模式 | 成員函式指標 void *state)();,主迴圈 (this->*state)(; |
訊息傳遞框架的類別結構(代碼 C.1–C.5)
| 類別 | 角色 | 關鍵設計 |
|---|---|---|
message_base |
佇列項的基底 struct | 只有虛解構子,讓異質訊息能放進同一條佇列 |
wrapped_message<Msg> |
各訊息型別的包裝 | 繼承 message_base,成員 Msg contents |
queue |
訊息佇列 | std::mutex + std::condition_variable + std::queue<std::shared_ptr<message_base>>;push() 打包並 notify_all(),wait_and_pop() 佇列空時阻塞 |
sender |
佇列指標的輕量包裝 | 只持有 queue*;複製 sender 只複製指標;send() 即 q->push(msg) |
receiver |
擁有佇列本體 | operator sender() 隱式轉出發送端;wait() 回傳 dispatcher 開始調度 |
dispatcher |
調度鏈的終點 | 不可複製、可移動;處理 close_queue(擲例外);handle<Msg>(f) 產生 TemplateDispatcher |
TemplateDispatcher<Prev,Msg,Func> |
鏈上的一環 | 記住前一個調度器 prev 與處理函式 f;可再 .handle<>() 串接 |
(擁有) (只持有指標,可自由複製)
receiver ───────────► queue ◄──────────────────────── sender
│ 佇列內容:std::shared_ptr<message_base>
│ wait() push() 打包成 wrapped_message<T> 並 notify_all()
▼
dispatcher ──handle<M1>(f1)──► TemplateDispatcher<dispatcher,M1,F1>
(鏈的終點, │
負責 close_queue) └──handle<M2>(f2)──► TemplateDispatcher<...,M2,F2>
(整條鏈都是臨時物件,鏈尾解構時才真正等待)
- 佇列以「指向基底類別的指標」儲存訊息,
message_base沒有任何資料成員,取用內容前必須轉回wrapped_message<T>*—— 型別匹配因此落在 dispatch 階段。 sender預設建構為nullptr(send()對空指標靜默不動作);訊息 struct 內夾帶mutable messaging::sender atm_queue即形成回覆通道。
dispatch 機制與 close_queue 結束(代碼 C.4–C.5)
wait() 回傳的 dispatcher 是臨時物件,析構函式才是真正做事的地方:未被鏈接(chained==false)的那一個在解構時呼叫 wait_and_dispatch() 進入等待迴圈。
訊息彈出後,從「鏈尾」往「鏈頭」匹配:
TD<M2>::dispatch ──不符──► TD<M1>::dispatch ──不符──► dispatcher::dispatch
│ dynamic_cast 符合 │ 符合 │
▼ ▼ ├─ 是 close_queue → throw close_queue()
f2(contents) → true f1(contents) → true └─ 其他 → return false(訊息被丟棄,
(跳出迴圈,等下一次 wait) 迴圈繼續等下一則)
chained旗標:handle<>()建立新環節時把前一環的chained設為 true;移動建構也會把來源標記為 chained。確保整條鏈只有最後一環會在解構時等待訊息。TemplateDispatcher::dispatch():以dynamic_cast<wrapped_message<Msg>*>檢查型別,符合就呼叫f(wrapper->contents)回傳 true;不符則return prev->dispatch(msg);沿鏈回溯。dispatcher::dispatch()(鏈頭/預設處理器):只認close_queue,收到就throw close_queue();;其他訊息回傳 false → 該訊息直接丟棄,迴圈繼續。- 兩個類別的解構子都標記
noexcept(false):C++11 起解構子預設noexcept(true),若不標記,close_queue例外逸出解構子會直接std::terminate()。
各角色的 run() 都是 for(;;) 無窮迴圈包在 try{...}catchclose_queue const&){} 中;done() 對自己的佇列送出 close_queue,例外一路穿出 wait_and_dispatch() 與 run( 的迴圈,執行緒乾淨收尾。
只要鏈上沒有對應的 handle<Msg>(),該訊息被 dispatcher::dispatch() 回傳 false 後即被拋棄,不會排隊等下一個狀態處理——狀態機設計時必須在正確狀態掛上正確的 handler。
ATM 三個角色的協作(代碼 C.6–C.10)
三個角色各自擁有 receiver、各跑一條 std::thread,彼此只透過 sender 傳訊息(main 以 get_sender() 取得彼此的發送端來組裝);鍵盤輸入由 main 執行緒轉成訊息餵給 atm。
鍵盤輸入(main 執行緒,getchar 迴圈)
│ card_inserted / digit_pressed / withdraw_pressed /
│ balance_pressed / cancel_pressed
▼
┌──────────────┐ verify_pin / withdraw / get_balance / ┌──────────────┐
│ atm │ withdrawal_processed / cancel_withdrawal │ bank_machine │
│ (狀態機) │ ───────────────────────────────────────────►│ (餘額 199, │
│ │ ◄───────────────────────────────────────────│ PIN="1937") │
└──────┬───────┘ pin_verified / pin_incorrect / └──────────────┘
│ withdraw_ok / withdraw_denied / balance
│ issue_money / eject_card / display_*
▼
┌───────────────────┐
│ interface_machine │ ──► std::cout(每次輸出以互斥鎖 iom 保護)
└───────────────────┘
| 角色 | 職責 | 處理的訊息(節選) |
|---|---|---|
atm |
狀態機,驅動整個流程 | 依當前狀態不同而異(見下節) |
bank_machine |
帳務邏輯:驗 PIN、查餘額、扣款 | verify_pin、withdraw、get_balance、withdrawal_processed、cancel_withdrawal;透過訊息內的 atm_queue 回覆 |
interface_machine |
硬體介面:顯示訊息、出鈔、退卡 | issue_money、display_*、eject_card;無狀態,單一 handle 鏈 |
- 需要回覆的請求訊息(
withdraw、verify_pin、get_balance)都內嵌mutable messaging::sender atm_queue,bank 直接對它send()—— 請求自帶回信地址,bank 無須認識 atm。 - 結束流程:main 收到
q後依序呼叫三個角色的done()(各送一則close_queue),再逐一join()。
atm 狀態機:成員函式指標模式(代碼 C.7)
class atm {
void *state)(; // 當前狀態 = 成員函式指標
void run() {
state = &atm::waiting_for_card;
try {
for(;;) (this->*state)(); // 每輪執行當前狀態函式
} catchclose_queue const& {}
}
};
每個狀態是一個成員函式:在裡面 incoming.wait().handle<...>() 等待該狀態關心的訊息,handler 中做動作並以 state = &atm::下一狀態; 完成轉移——狀態 × 訊息 → 動作 + 新狀態,正是狀態機的轉移表。
正常提款路徑:
waiting_for_card ─card_inserted─► getting_pin ─集滿4碼─► verifying_pin
▲ │ pin_verified
│ (eject_card 後回到起點) ▼
done_processing ◄── withdraw_ok/denied 或 cancel ── process_withdrawal
▲ ▲ withdraw_pressed
└────── cancel_pressed(幾乎任何狀態) ────── wait_for_action ──balance_pressed─► process_balance
| 狀態 | 等待的訊息 → 動作與轉移 |
|---|---|
waiting_for_card |
card_inserted → 記帳號、清空 PIN、顯示輸入 PIN → getting_pin |
getting_pin |
digit_pressed 累積至 4 碼 → 送 verify_pin(附回覆通道)→ verifying_pin;clear_last_pressed 刪一碼;cancel_pressed → done_processing |
verifying_pin |
pin_verified → wait_for_action;pin_incorrect → 顯示錯誤 → done_processing |
wait_for_action |
withdraw_pressed → 送 withdraw → process_withdrawal;balance_pressed → 送 get_balance → process_balance;cancel_pressed → done_processing |
process_withdrawal |
withdraw_ok → 出鈔、通知 bank 完成 → done_processing;withdraw_denied → 顯示餘額不足 → done_processing;cancel_pressed → 通知 bank 取消 → done_processing |
process_balance |
balance → 顯示餘額 → wait_for_action;cancel_pressed → done_processing |
done_processing |
(不等訊息)退卡 → waiting_for_card |
每個狀態機的資料(帳號、PIN、餘額)只被自己那條執行緒碰觸,執行緒間只交換不可變的訊息副本——這就是第 4 章 CSP/Actor 風格「以通訊共享資料,而非以共享資料通訊」的完整落地,詳見 04-Synchronizing-Operations/04-Functional-and-Message-Passing-Styles。
Exam/Test Patterns
| 情境/關鍵字 | 答案 |
|---|---|
| 「訊息迴圈如何結束」 | 送 close_queue → dispatcher::dispatch() 擲例外 → run() 的 catch 收尾 |
| 「dispatcher 何時真正等待訊息」 | 解構時;chained 旗標確保只有鏈尾那個環節等待 |
| 「異質訊息如何判別型別」 | 佇列存 shared_ptr<message_base>,以 dynamic_cast<wrapped_message<Msg>*> 匹配 |
| 「訊息沒有任何 handle 匹配」 | 沿 prev 鏈回溯到 dispatcher,回傳 false → 丟棄,繼續等待 |
| 「sender 與 receiver 的差別」 | sender 只持 queue*(可複製、可任意分發);receiver 擁有 queue |
| 「請求如何拿到回覆」 | 訊息 struct 內嵌 mutable messaging::sender atm_queue(回信地址) |
| 「解構子會擲例外怎麼辦」 | 必須標 noexcept(false),否則例外逸出即 std::terminate() |
| 「狀態機的實作模式」 | 成員函式指標 void *state)(); + 主迴圈 (this->*state)(; |
| 「為何無資料競爭」 | 各狀態機資料只屬於自己執行緒,執行緒間只傳訊息(CSP 風格) |
Related Notes
- 12-Appendix/01-Cpp11-Language-Features
- 12-Appendix/02-Concurrency-Library-Comparison
- 12-Appendix/Practice-Appendix
- 04-Synchronizing-Operations/04-Functional-and-Message-Passing-Styles — 第 4 章的 CSP/訊息傳遞風格正文
- 04-Synchronizing-Operations/01-Condition-Variables-and-Thread-Safe-Queue — 框架中
queue的條件變數實作基礎