關於 shared_ptr 的線程安全

原文地址:https://www.cnblogs.com/gqtcgq/p/7492772.html

    shared_ptr的引用計數本身是安全且無鎖的,但對象的讀寫則不是,因爲 shared_ptr 有兩個數據成員(指向被管理對象的指針,和指向控制塊的指針),讀寫操作不能原子化。根據文檔(http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm#ThreadSafety), shared_ptr 的線程安全級別和內建類型、標準庫容器、std::string 一樣,即:

• 一個 shared_ptr 對象實體可被多個線程同時讀取(文檔例1);

• 兩個 shared_ptr 對象實體可以被兩個線程同時寫入(例2),“析構”算寫操作;

• 如果要從多個線程讀寫同一個 shared_ptr 對象,那麼需要加鎖(例3~5)。

請注意,以上是 shared_ptr 對象本身的線程安全級別,不是它管理的對象的線程安全級別。

 
 
本文具體分析一下爲什麼“因爲 shared_ptr 有兩個數據成員,讀寫操作不能原子化”使得多線程讀寫同一個 shared_ptr 對象需要加鎖。這個在我看來顯而易見的結論似乎也有人抱有疑問,那將導致災難性的後果,值得我寫這篇文章。本文以 boost::shared_ptr 爲例,與 std::shared_ptr 可能略有區別。

 
1:shared_ptr 的數據結構

shared_ptr 是引用計數型(reference counting)智能指針,幾乎所有的實現都採用在堆(heap)上放個計數值(count)的辦法(除此之外理論上還有用循環鏈表的辦法,不過沒有實例)。具體來說,shared_ptr 包含兩個成員,一個是指向 Foo 的指針 ptr,另一個是 ref_count 指針(其類型不一定是原始指針,有可能是 class 類型,但不影響這裏的討論),指向堆上的 ref_count 對象。ref_count 對象有多個成員,具體的數據結構如圖 1 所示,其中 deleter 和 allocator 是可選的。
在這裏插入圖片描述圖 1:shared_ptr 的數據結構。

 
爲了簡化並突出重點,後文只畫出 use_count 的值:

以上是 shared_ptr x(new Foo); 對應的內存數據結構。

如果再執行 shared_ptr y = x; 那麼對應的數據結構如下。
在這裏插入圖片描述

但是 y=x 涉及兩個成員的複製,這兩步拷貝不會同時(原子)發生。

中間步驟 1,複製 ptr 指針:
在這裏插入圖片描述

中間步驟 2,複製 ref_count 指針,導致引用計數加 1:

在這裏插入圖片描述

步驟1和步驟2的先後順序跟實現相關(因此步驟 2 裏沒有畫出 y.ptr 的指向),我見過的都是先1後2。

既然 y=x 有兩個步驟,如果沒有 mutex 保護,那麼在多線程裏就有 race condition。

 
2:多線程無保護讀寫 shared_ptr 可能出現的 race condition

考慮一個簡單的場景,有 3 個 shared_ptr 對象 x、g、n:

shared_ptr<Foo> g(new Foo); // 線程之間共享的 shared_ptr

shared_ptr<Foo> x; // 線程 A 的局部變量

shared_ptr<Foo> n(new Foo); // 線程 B 的局部變量

一開始,各安其事:

在這裏插入圖片描述

線程 A 執行 x = g; (即 read g),以下完成了步驟 1,還沒來及執行步驟 2。這時切換到了 B 線程。

在這裏插入圖片描述
同時編程 B 執行 g = n; (即 write g),兩個步驟一起完成了。先是步驟 1:

在這裏插入圖片描述

再是步驟 2:

在這裏插入圖片描述

這時 Foo1 對象已經銷燬,x.ptr 成了空懸指針!

最後回到線程 A,完成步驟 2:

在這裏插入圖片描述

多線程無保護地讀寫 g,造成了“x 是空懸指針”的後果。這正是多線程讀寫同一個 shared_ptr 必須加鎖的原因。

 
當然,race condition 遠不止這一種,其他線程交織(interweaving)有可能會造成其他錯誤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章