訊息傳遞框架與 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>
                                            (整條鏈都是臨時物件,鏈尾解構時才真正等待)

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)                                      迴圈繼續等下一則)
以例外結束訊息迴圈

各角色的 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_pinwithdrawget_balancewithdrawal_processedcancel_withdrawal;透過訊息內的 atm_queue 回覆
interface_machine 硬體介面:顯示訊息、出鈔、退卡 issue_moneydisplay_*eject_card;無狀態,單一 handle 鏈

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_presseddone_processing
verifying_pin pin_verifiedwait_for_action;pin_incorrect → 顯示錯誤 → done_processing
wait_for_action withdraw_pressed → 送 withdrawprocess_withdrawal;balance_pressed → 送 get_balanceprocess_balance;cancel_presseddone_processing
process_withdrawal withdraw_ok → 出鈔、通知 bank 完成 → done_processing;withdraw_denied → 顯示餘額不足 → done_processing;cancel_pressed → 通知 bank 取消 → done_processing
process_balance balance → 顯示餘額 → wait_for_action;cancel_presseddone_processing
done_processing (不等訊息)退卡 → waiting_for_card
為什麼這個設計沒有共享狀態問題

每個狀態機的資料(帳號、PIN、餘額)只被自己那條執行緒碰觸,執行緒間只交換不可變的訊息副本——這就是第 4 章 CSP/Actor 風格「以通訊共享資料,而非以共享資料通訊」的完整落地,詳見 04-Synchronizing-Operations/04-Functional-and-Message-Passing-Styles


Exam/Test Patterns

情境/關鍵字 答案
「訊息迴圈如何結束」 close_queuedispatcher::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 風格)