C++0x(即後來的 C++11)在多線程支援上引入了豐富的機制。本文聚焦於初始化階段的資料保護與線程間事件等待兩大核心場景。
若資料需在初始化時受保護,使用 mutex 會導致初始化結束後仍產生不必要的同步開銷。C++0x 提供三種更優雅的方案:
若建構函數以 constexpr 宣告並滿足常量初始化條件,靜態儲存期物件會在靜態初始化階段完成,早於任何其他代碼執行。這對 std::mutex 尤其適合,消除了全域 mutex 初始化競爭的可能:
class my_class {
int i;
public:
constexpr my_class() : i(0) {}
my_class(int i_) : i(i_) {}
void do_stuff();
};
my_class x; // 靜態初始化,安全
my_class y(42 + foo()); // 動態初始化,需注意
void f() { y.do_stuff(); } // y 是否已初始化?
C++0x 保證區塊作用域的靜態變量在函數首次被呼叫時初始化,若另一線程在初始化完成前嘗試呼叫,會自動等待:
void bar() {
static my_class z(42 + foo()); // 初始化過程線程安全
z.do_stuff();
}
適用於動態建立物件的場景。std::call_once 與 std::once_flag 協作,確保指定函數只執行一次:
my_class* p = nullptr;
std::once_flag p_flag;
void create_instance() { p = new my_class(42 + foo()); }
void baz() {
std::call_once(p_flag, create_instance);
p->do_stuff();
}
std::call_once 同樣支援函數物件與多參數,預設傳值,需傳引用時使用 std::ref。
線程間共享資料時,常需一個線程等待另一個線程完成某操作。最簡單的輪詢寫法(睡眠 + 檢查旗標)有兩大缺點:
條件變量(condition_variable) 讓線程高效休眠直到收到通知,CPU 佔用率可降至 0:
std::mutex m;
std::condition_variable cond;
bool data_ready = false;
void foo() {
std::unique_lock<std::mutex> lk(m);
while (!data_ready) {
cond.wait(lk); // 釋放鎖並休眠,被喚醒後重新獲取鎖
}
process_data();
}
void set_data_ready() {
std::lock_guard<std::mutex> lk(m);
data_ready = true;
cond.notify_one(); // 喚醒一個等待線程
}
注意:條件變量可能發生虛假喚醒,因此必須在迴圈中檢查條件。更簡潔的寫法是使用帶 predicate 的 wait:
void foo() {
std::unique_lock<std::mutex> lk(m);
cond.wait(lk, []{ return data_ready; }); // 標準庫自動處理虛假喚醒
process_data();
}
掌握這些機制,能讓多線程程式在初始化與同步協作上更安全、高效、優雅。