Item 16:讓 const 成員函式具備執行緒安全 (Make const Member Functions Thread Safe)
Overview Table
| 重點 | 說明 |
|---|---|
| 核心原則 | const 成員函式代表「讀取操作」,多執行緒會不加同步地並行呼叫,因此必須是 thread safe |
| 問題來源 | mutable 成員(快取、計數器等)讓「概念上 const」的函式實際上會寫入資料成員 → data race → undefined behavior |
| 單一變數同步 | 用 std::atomic——通常比 mutex 便宜 |
| 兩個以上變數需一起操作 | 用 std::mutex(如 std::lock_guard 上鎖)——多個 atomic 無法保證整體一致性 |
| 副作用 | std::mutex 與 std::atomic 都是 move-only,加入後類別失去可複製性(仍可 move) |
| 唯一例外 | 能確定該物件絕不會被多執行緒同時使用時,才可省去同步成本 |
const ≠ Thread Safe:mutable 快取引發 data race
概念上不修改物件的函式宣告成 const 完全正確,但若內部透過 mutable 成員做快取,兩個執行緒同時呼叫就會同時讀寫同一塊記憶體:
class Polynomial {
public:
using RootsType = std::vector<double>; // 存放多項式根的容器(Item 9 的 alias)
RootsType roots() const // 概念上是唯讀 → 宣告 const 合理
{
if (!rootsAreValid) { // 快取失效才計算
// ... 計算根並存入 rootVals(昂貴運算)
rootsAreValid = true; // 寫入 mutable 成員!
}
return rootVals;
}
private:
mutable bool rootsAreValid{ false }; // mutable:const 函式內仍可修改
mutable RootsType rootVals{};
};
Thread 1: p.roots() Thread 2: p.roots()
| |
v v
讀 rootsAreValid ──┐ ┌── 讀 rootsAreValid
├── 同一記憶體 ──┤
寫 rootVals ───────┘ └── 寫 rootsAreValid
|
v
無同步的並行讀寫 = data race = undefined behavior
修正:加入 mutable std::mutex,以 std::lock_guard 涵蓋整個「檢查—計算—寫入」流程:
class Polynomial {
public:
using RootsType = std::vector<double>;
RootsType roots() const
{
std::lock_guard<std::mutex> g(m); // 上鎖(離開作用域自動解鎖)
if (!rootsAreValid) {
// ... 計算並存入 rootVals
rootsAreValid = true;
}
return rootVals;
}
private:
mutable std::mutex m; // mutable:lock/unlock 是 non-const 操作
mutable bool rootsAreValid{ false };
mutable RootsType rootVals{};
};
Important
std::mutex 必須宣告 mutable:lock()/unlock() 是 non-const 成員函式,而在 const 成員函式中 m 會被視為 const 物件。
Warning
加入 std::mutex(或 std::atomic)的副作用:兩者皆為 move-only type(可 move、不可 copy),所以類別會喪失可複製性,只剩可移動性。
單一變數用 std::atomic:更輕量的選擇
只需同步單一變數或記憶體位置(如呼叫次數計數器)時,std::atomic 通常比 mutex 便宜(見 Item 40):
class Point { // 2D 點
public:
double distanceFromOrigin() const noexcept // 不拋例外 → noexcept(Item 14)
{
++callCount; // 原子遞增,不需 mutex
return std::sqrt((x * x) + (y * y));
}
private:
mutable std::atomic<unsigned> callCount{ 0 }; // move-only → Point 也變 move-only
double x, y;
};
Warning
「atomic 比 mutex 便宜」並非絕對:實際效能取決於硬體與標準庫中 mutex 的實作方式。
兩個以上變數:一對 atomic 是陷阱,該用 mutex
把「快取值 + 快取有效旗標」拆成兩個 std::atomic,無論寫入順序如何都有問題:
class Widget {
public:
int magicValue() const
{
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2; // uh oh, part 1
cacheValid = true; // uh oh, part 2
return cachedValue;
}
}
private:
mutable std::atomic<bool> cacheValid{ false };
mutable std::atomic<int> cachedValue;
};
| 賦值順序 | 競態情境 | 後果 |
|---|---|---|
先 cachedValue 再 cacheValid |
Thread 1 剛算完、尚未設旗標,Thread 2(可能多個)看到 cacheValid == false |
重複昂貴計算——只是效率差,結果仍正確 |
先 cacheValid 再 cachedValue |
Thread 2 看到旗標為 true,但 Thread 1 還沒寫入 cachedValue |
回傳未初始化的錯誤值——更糟 |
教訓:需要作為一個單位操作的兩個以上變數/記憶體位置,就該用 mutex:
class Widget {
public:
int magicValue() const
{
std::lock_guard<std::mutex> guard(m); // 上鎖,整段臨界區受保護
if (cacheValid) return cachedValue;
else {
auto val1 = expensiveComputation1();
auto val2 = expensiveComputation2();
cachedValue = val1 + val2;
cacheValid = true;
return cachedValue;
}
} // 解鎖
private:
mutable std::mutex m;
mutable int cachedValue; // 不再需要 atomic
mutable bool cacheValid{ false }; // 不再需要 atomic
};
Tip
選擇準則一句話:一個變數 → std::atomic;一個單位內多個變數 → std::mutex。
Warning
本 Item 的前提是物件可能被多執行緒同時執行 const 成員函式。若能保證絕不發生(如專為單執行緒設計的類別),thread safety 就無關緊要,可省下 mutex/atomic 的成本與 move-only 副作用。但這種「保證無並行」的情境越來越罕見——保守的做法仍是讓 const 成員函式 thread safe。
Exam/Test Patterns
| 情境關鍵字 | 答案 |
|---|---|
const 成員函式 + mutable 快取 + 多執行緒同時呼叫 |
data race → undefined behavior;const 不保證 thread safe |
std::mutex 放在 const 成員函式中要怎麼宣告 |
mutable std::mutex(lock/unlock 是 non-const 操作) |
加了 std::mutex / std::atomic 後類別不能複製 |
兩者是 move-only type,含有它們的類別跟著變 move-only |
| 只需同步一個計數器/單一變數 | 用 std::atomic,通常比 mutex 便宜 |
| 兩個以上變數需一起更新(值 + 有效旗標) | 用 mutex;一對 atomic 會造成重複計算或回傳錯誤值 |
先設 cacheValid = true 再寫 cachedValue |
其他執行緒可能讀到尚未賦值的 cachedValue(最糟情境) |
| 什麼時候可以不做 thread safe | 能確定永遠不會在並行情境中使用該函式時(越來越罕見) |
Related Notes
- 04-Moving-To-Modern-Cpp/08-Noexcept —
distanceFromOrigin範例宣告noexcept;不拋例外的函式應如此標註 - 04-Moving-To-Modern-Cpp/09-Constexpr — 同章前一個 Item;constexpr 與 const 成員函式在 C++14 的互動
- 04-Moving-To-Modern-Cpp/11-Special-Member-Function-Generation — mutex/atomic 成員使 copy 操作被抑制,影響特殊成員函式的生成
- 08-Concurrency-API/06-Atomic-Vs-Volatile — Item 40 深入說明
std::atomic的保證與適用場景 - 04-Moving-To-Modern-Cpp/03-Alias-Declarations —
using RootsType = ...的 alias declaration 寫法