對於一個很大的專案來說,我一直在思考的是「穩定性」和「強健性」這兩件事情。在我Gap Year前,專案一直面臨到一些偶發的崩潰狀態,這對我來說並不是感到太意外,只是在思考究竟哪裡應該注意而未被注意。
夢回多年前我第一次跟微軟一起在合作寫第一個LF OpenBMC的系統的時候,當時也經常發生一些崩潰的狀態,因為那時還沒有phosphor dbus interface這個Repo的誕生,也是大家剛開始從Sysinit的環境轉換到systemd的階段,對於Dbus這個東西既愛又恨。崩潰的點經常就是我們acquired了之後沒有release bus....或者是我們過於頻繁的去access dbus而不是一次把資訊讀回來後再分析。這牽涉到時間複雜度的問題,這也難怪很多大廠的考試總愛考這部分,因為...悲劇總在不經意間發生。但是有多少人能在考試的時候很強又在真實工作的時候運用出來,這我就不知道,畢竟我也覺得自己只能一直很努力的學習,怎麼學總會感到不足,漏掉...這種很想K自己一拳的時候。 (哈 但總體上我是樂觀的...尻自己一拳之後我會立刻去吃一個提拉米蘇,重新活過來)
喔好~但就像smart pointer的誕生一樣,現在記憶體的allocate與free好像已經很便利了,只要使用摩登現代C++並好好的運用,出錯的機率不高。然後我偶像之一Patrick又寫出了phosphor dbus interface之後,我感覺像是看到一本教科書,你不太需要深入去理解Dbus你只要會用就好。但這樣夠嗎?下一個OpenBMC的時代,大家會面臨的問題是什麼?為什麼系統還是有崩潰的時候?我不確定現在還有多少的全域變數正在被使用,早期在我的記憶中,有時候我們會為了區別自己的系統與別人系統的不同而去enable不同的功能,然後就會出現不少像是"is_enabled", "activate_XXX_func"等等之類的這種痊癒變數,有些是從bitbake時的BB file中設定,再被定義到cpp code中。我們最近一起在研究的phosphor-pid-control(以下簡稱 swampd)也是一個基於 Boost.Asio 的非同步單執行緒守護process (之前看dbus-sensor的時候也提到過)。其核心邏輯圍繞著週期性的 PID 運算展開。目前還是會看到一些「全域旗標」或「共用指標」進行狀態傳遞的現象,例如 isCanceling 指標與 tuningEnabled 等全域變數。
swampd 目前使用 bool 或 int 作為全域旗標卻沒有傳出災情是因為它運作在單執行緒的非同步事件循環中。如果是多執行緒的話,純全域變數將面臨 Data Race 的威脅。根據 C++ 標準,如果兩個執行緒同時存取同一個非原子變數,且其中至少有一個是寫入動作,該行為即為「未定義」。這可能導致:
- 數值撕裂 (Value Tearing):在 32 位元架構存取 64 位元變數時,可能只讀到一半的修改。
- 不可預測的狀態機崩潰:系統判定
isCanceling為真,但相關的緩存數據卻尚未重新整理。
為什麼 std::atomic 更好?
前面寫了這麼多,只是想鋪陳最近看到別人用std::atomic這個東西,覺得新奇想跟大家分享一下而已。std::atomic 不僅僅是「執行一個原子操作」,它提供了三大核心保證:
- 原子性 (Atomicity):保證操作不可分割,不會讀到半成品的數值。
- 可見性 (Visibility):配合總線鎖或緩存一致性指令,確保修改後的數值能被其他核心偵測。
- 順序約束 (Ordering Constraints):透過記憶體屏障(Memory Barriers),限制編譯器與 CPU 的重排行為。
在 std::atomic 中,我們可以根據需求選擇最合適的「嚴格程度」:
memory_order_relaxed:僅保證原子性,不保證順序。適合單純計數(如統計數據)。memory_order_acquire / memory_order_release:這是最推薦用於 swampd 這種「旗標式同步」的模式。- Release:確保這行寫入之前的動作全部完成。
- Acquire:確保這行讀取之後的動作看到最新的狀態。
memory_order_seq_cst:預設模式,保證全域一致性順序,但開銷最大。
實際來使用看看
這個code模擬一下我們正在研究的風扇控制系統架構:有一個 Worker thread 在跑迴圈,而 Main 負責在 1 秒後下達停止指令。
#include <iostream>
#include <atomic>
#include <thread>
#include <chrono>
class FanService {
public:
// static 關鍵字代表這是類別共享的旗標,模擬專案中的 global flag
static std::atomic<bool> isCanceling;
static std::atomic<int> processedCount;
void runControlLoop() {
std::cout << "[Worker] PID 控制循環啟動...\n";
while (true) {
// --- 用法 1: memory_order_acquire (中量級) ---
// 保證看到主線程在 store 之前所做的所有記憶體修改
if (isCanceling.load(std::memory_order_acquire)) {
std::cout << "[Worker] 接收到優雅停止訊號,準備關閉...\n";
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模擬 100ms 週期
// --- 用法 2: memory_order_relaxed (輕量級) ---
// 單純累加次數,不涉及其它狀態同步,效能最高
processedCount.fetch_add(1, std::memory_order_relaxed);
}
}
};
// 初始化靜態成員
std::atomic<bool> FanService::isCanceling{false};
std::atomic<int> FanService::processedCount{0};
int main() {
FanService service;
std::thread workerThread(&FanService::runControlLoop, &service);
std::this_thread::sleep_for(std::chrono::seconds(1)); // 讓它跑一秒
std::cout << "[Main] 下達停止指令...\n";
// --- memory_order_release ---
// 確保這行之前的動作都已完成,然後才舉旗
FanService::isCanceling.store(true, std::memory_order_release);
workerThread.join();
std::cout << "[Main] 測試完成,總迭代次數:" << FanService::processedCount.load() << "\n";
return 0;
}
isCanceling 用了 Acquire/Release:因為這涉及「同步」。一旦旗標舉起,Worker 必須立刻看到主執行緒最新的變動。processedCount 用了 Relaxed:因為這只是計數,稍微晚個 1 微秒看到總數也沒關係,這樣可以省下維護記憶體順序的昂貴效能成本。
執行的command:
g++ -std=c++11 atomic_demo.cpp -o atomic_demo -pthread && ./atomic_demo
這就是為什麼 boost::asio 這麼受歡迎,因為在單執行緒下,io.run() 幫我們擋掉了大半的併發問題。但要注意,『非同步』不代表『絕對安全』。一旦工作量變大,你開始考慮使用多執行緒來跑 io.run() 時,那些散落在各處的回呼(Callbacks)就像是同時在不同車道賽車,隨時可能撞在一起。如果你還在使用傳統的全域變數,卻沒有 std::atomic 的保護,那悲劇發生的機率就會大幅增加。
你們也來跟我分享,最近還有什麼該學的摩登現代C++用法吧!畢竟我已不再年輕,而C++的進化速度總是超乎我的預期,現在C++的內容真的跟我當年讀大學第一次接觸的差好多喔!但是好像越來越棒了!跟大家分享,我最近覺得研究UNIX系統中原本就存在的command他是怎麼implement的,也是超有趣的一件事情。希望大家也會越來越喜歡嵌入式的世界和領域喔!我們一起加油吧!












