shared_ptr的線程安全性


一:

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object. If multiple threads of execution access the same shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.

多線程環境下,調用不同shared_ptr實例的成員函數是不需要額外的同步手段的,即使這些shared_ptr擁有的是同樣的對象。但是如果多線程訪問(有寫操作)同一個shared_ptr,則需要同步,否則就會有race condition 發生。也可以使用 shared_ptr overloads of atomic functions 來防止race condition的發生。

 

To satisfy thread safety requirements, the reference counters are typically incremented using an equivalent of std::atomic::fetch_add with std::memory_order_relaxed (decrementing requires stronger ordering to safely destroy the control block).

shared_ptr的引用計數本身是安全且無鎖的。

 

         http://en.cppreference.com/w/cpp/memory/shared_ptr

 

二:

It is only the control block itself which is thread-safe.

 

I put that on its own line for emphasis. The contents of the shared_ptr are not thread-safe, nor is writing to the same shared_ptr instance. Here's something to demonstrate what I mean:

 

// In main()

shared_ptr<myClass> global_instance = make_shared<myClass>();

// (launch all other threads AFTER global_instance is fully constructed)

 

//In thread 1

shared_ptr<myClass> local_instance = global_instance;

This is fine, in fact you can do this in all threads as much as you want. And then when local_instance is destructed (by going out of scope), it is also thread-safe. Somebody can be accessing global_instance and it won't make a difference. The snippet you pulled from msdn basically means "access to the control block is thread-safe" so other shared_ptr<> instances can be created and destroyed on different threads as much as necessary.

 

//In thread 1

local_instance = make_shared<myClass>();

This is fine. It will affect the global_instance object, but only indirectly. The control block it points to will be decremented, but done in a thread-safe way.  local_instance will no longer point to the same object (or control block) as global_instance does.

 

//In thread 2

global_instance = make_shared<myClass>();

This is almost certainly not fine if global_instance is accessed from any other threads (which you say you're doing). It needs a lock if you're doing this because you're writing to wherever global_instance lives, not just reading from it. So writing to an object from multiple threads is bad unless it's you have guarded it through a lock. So you can read from global_instance the object by assigning new shared_ptr<> objects from it but you can't write to it.

結論:多個線程同時讀同一個shared_ptr對象是線程安全的,但是如果是多個線程對同一個shared_ptr對象進行讀和寫,則需要加鎖。

 

// In thread 3

*global_instance = 3;

int a = *global_instance;

 

// In thread 4

*global_instance = 7;

The value of a is undefined. It might be 7, or it might be 3, or it might be anything else as well. The thread-safety of the shared_ptr<> instances only applies to managing shared_ptr<> instances which were initialized from each other, not what they're pointing to.

多線程讀寫shared_ptr所指向的同一個對象,不管是相同的shared_ptr對象,還是不同的shared_ptr對象,也需要加鎖保護。例子如下:

shared_ptr<long> global_instance = make_shared<long>(0);
std::mutex g_i_mutex;

void thread_fcn()
{
    //std::lock_guard<std::mutex> lock(g_i_mutex);

    //shared_ptr<long> local = global_instance;

    for(int i = 0; i < 100000000; i++)
    {
        *global_instance = *global_instance + 1;
        //*local = *local + 1;
    }
}

int main(int argc, char** argv)
{
    thread thread1(thread_fcn);
    thread thread2(thread_fcn);

    thread1.join();
    thread2.join();

    cout << "*global_instance is " << *global_instance << endl;

    return 0;
}

在線程函數thread_fcn的for循環中,2個線程同時對*global_instance進行加1的操作。這就是典型的非線程安全的場景,最后的結果是未定的,運行結果如下:

*global_instance is 197240539

 

如果使用的是每個線程的局部shared_ptr對象local,因為這些local指向相同的對象,因此結果也是未定的,運行結果如下:

*global_instance is 160285803

 

因此,這種情況下必須加鎖,將thread_fcn中的第一行代碼的注釋去掉之后,不管是使用global_instance,還是使用local,得到的結果都是:

*global_instance is 200000000

https://stackoverflow.com/questions/14482830/stdshared-ptr-thread-safety

 

三:為什么多線程讀寫 shared_ptr 要加鎖?

以下內容,摘自陳碩的 http://blog.csdn.net/solstice/article/details/8547547

 

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> 包含兩個成員,一個是指向 Foo 的指針 ptr,另一個是 ref_count 指針(其類型不一定是原始指針,有可能是 class 類型,但不影響這里的討論),指向堆上的 ref_count 對象。ref_count 對象有多個成員,具體的數據結構如圖 1 所示,其中 deleter 和 allocator 是可選的。

 

圖 1:shared_ptr 的數據結構。

 

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

 

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

 

如果再執行 shared_ptr<Foo> 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<Foo> 對象 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)有可能會造成其他錯誤。

 

雜項

shared_ptr 作為 unordered_map 的 key

如果把 boost::shared_ptr 放到 unordered_set 中,或者用於 unordered_map 的 key,那么要小心 hash table 退化為鏈表。

http://stackoverflow.com/questions/6404765/c-shared-ptr-as-unordered-sets-key/12122314#12122314

 

直到 Boost 1.47.0 發布之前,unordered_set<std::shared_ptr<T> > 雖然可以編譯通過,但是其 hash_value 是 shared_ptr 隱式轉換為 bool 的結果。也就是說,如果不自定義hash函數,那么 unordered_{set/map} 會退化為鏈表。https://svn.boost.org/trac/boost/ticket/5216

 

Boost 1.51 在 boost/functional/hash/extensions.hpp 中增加了有關重載,現在只要包含這個頭文件就能安全高效地使用 unordered_set<std::shared_ptr> 了。

 

這也是 muduo 的 examples/idleconnection 示例要自己定義 hash_value(const boost::shared_ptr<T>& x) 函數的原因(書第 7.10.2 節,p.255)。因為 Debian 6 Squeeze、Ubuntu 10.04 LTS 里的 boost 版本都有這個 bug。

 

為什么要盡量使用 make_shared()

為了節省一次內存分配,原來 shared_ptr<Foo> x(new Foo); 需要為 Foo 和 ref_count 各分配一次內存,現在用 make_shared() 的話,可以一次分配一塊足夠大的內存,供 Foo 和 ref_count 對象容身。數據結構是:

 

不過 Foo 的構造函數參數要傳給 make_shared(),后者再傳給 Foo::Foo(),這只有在 C++11 里通過 perfect forwarding 才能完美解決。

 

(.完.)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM