首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

多執行緒環境下讀寫 shared_ptr 必須加鎖的根本原因分析

     編輯:關於C++

《Linux 多執行緒伺服端程式設計》書中指出:shared_ptr 參考計數本身無鎖安全,但對象讀寫非安全,因有兩個資料成員,讀寫無法原子化。線程安全級別同內建類型/標準容器:多線程可同時讀同一物件、可同時寫不同物件、同一物件多線程讀寫必須加鎖。本文深入分析為何「兩個資料成員導致非原子」會造成災難性後果。

shared_ptr 內部資料結構

shared_ptr<Foo> 含兩成員:

  • ptr:指向 Foo 物件
  • ref_count:指向堆上控制塊(含引用計數、弱引用計數、deleter、allocator)

圖示:

shared_ptr x(new Foo);     // x.ptr → Foo, x.ref_count → ControlBlock(count=1)
shared_ptr y = x;          // 複製兩個指標,計數變 2

複製操作的非原子性與 Race Condition

y = x 分兩步驟(順序視實作而定,通常先 ptr 後 ref_count):

  1. 複製 ptr 指標
  2. 複製 ref_count 指標 → 控制塊計數 +1

具體 Race Condition 情境

三個 shared_ptr:g (共享)、x (線程 A 局部)、n (線程 B 局部)

// 初始狀態
g → Foo1 (count=1)
x → 空
n → Foo2 (count=1)

// 線程 A: x = g (讀 g) - 完成步驟1 (複製 ptr),尚未步驟2
// 切換到線程 B: g = n (寫 g) - 完成兩步驟
//   步驟1: g.ptr = n.ptr (指向 Foo2)
//   步驟2: g.ref_count = n.ref_count, Foo1 計數歸零 → Foo1 被銷毀
// 切回線程 A: 完成步驟2 (複製舊 ref_count,計數+1)
// 結果:x.ptr 指向已銷毀的 Foo1 → 懸空指標!

其他 Race Condition

若 operator= 實作順序相反(先 ref_count 後 ptr),同樣存在競態:線程 A 複製 ref_count 後、複製 ptr 前,線程 B 修改 g 導致控制塊計數錯誤或物件提前銷毀。

雜項補充

shared_ptr 作為 unordered_map key 的效能陷阱

  • Boost 1.47 前:hash_value 為隱式轉 bool 結果,導致退化為鏈表
  • Boost 1.51+:包含 boost/functional/hash/extensions.hpp 即安全高效
  • muduo idleconnection 範例自訂 hash_value 因舊版 Boost 存此 bug

控制塊為何也存 ptr?

支援三大進階功能:

  1. 無需虛析構: 基類無虛析構時,shared_ptr<Derived> 轉 shared_ptr<Base>,控制塊記住 Derived 類型確保正確銷毀
  2. shared_ptr<void>: 可安全管理任意物件,delete 控制塊記錄的原始指標而非 void*
  3. 多重繼承位移: Base subobject 在 Derived 中位移非零時,shared_ptr<Base>.ptr 與控制塊 ptr 不同,銷毀時仍正確

為什麼推薦 make_shared()?

單次記憶體配置同時容納物件與控制塊,省一次配置開銷。資料結構:

[ ControlBlock | Foo Object ]  // 連續記憶體區塊

需 C++11 perfect forwarding 完美轉發建構參數。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved