1.9 再論shared_ptr 的線程安全
雖然我們借shared_ptr 來實現線程安全的對象釋放,但是shared_ptr 本身不是100% 線程安全的。它的引用計數本身是安全且無鎖的,但對象的讀寫則不是,因為shared_ptr 有兩個數據成員,讀寫操作不能原子化。根據文檔11,shared_ptr 的線程安全級別和內建類型、標准庫容器、std::string 一樣,即:
一個shared_ptr 對象實體可被多個線程同時讀取;
兩個shared_ptr 對象實體可以被兩個線程同時寫入,“析構”算寫操作;
如果要從多個線程讀寫同一個shared_ptr 對象,那么需要加鎖。
請注意,以上是shared_ptr 對象本身的線程安全級別,不是它管理的對象的線程安全級別。
要在多個線程中同時訪問同一個shared_ptr,正確的做法是用mutex 保護:
- MutexLock mutex; // No need for ReaderWriterLock
- shared_ptr<Foo> globalPtr;
- // 我們的任務是把globalPtr 安全地傳給doit()
- void doit(const shared_ptr<Foo>& pFoo);
globalPtr 能被多個線程看到,那么它的讀寫需要加鎖。注意我們不必用讀寫鎖,而只用最簡單的互斥鎖,這是為了性能考慮。因為臨界區非常小,用互斥鎖也不會阻塞並發讀。
為了拷貝globalPtr,需要在讀取它的時候加鎖,即:
- void read()
- {
- shared_ptr<Foo> localPtr;
- {
- MutexLockGuard lock(mutex);
- localPtr = globalPtr; // read globalPtr
- }
- // use localPtr since here,讀寫localPtr 也無須加鎖
- doit(localPtr);
- }
寫入的時候也要加鎖:
- void write()
- {
- shared_ptr<Foo> newPtr(new Foo); // 注意,對象的創建在臨界區之外
- {
- MutexLockGuard lock(mutex);
- globalPtr = newPtr; // write to globalPtr
- }
- // use newPtr since here,讀寫newPtr 無須加鎖
- doit(newPtr);
- }
注意到上面的read() 和write() 在臨界區之外都沒有再訪問globalPtr,而是用了一個指向同一Foo 對象的棧上shared_ptr local copy。下面會談到,只要有這樣的local copy 存在,shared_ptr 作為函數參數傳遞時不必復制,用reference to const 作為參數類型即可。另外注意到上面的new Foo 是在臨界區之外執行的,這種寫法通常比在臨界區內寫globalPtr.reset(new Foo) 要好,因為縮短了臨界區長度。如果要銷毀對象,我們固然可以在臨界區內執行globalPtr.reset(),但是這樣往往會讓對象析構發生在臨界區以內,增加了臨界區的長度。一種改進辦法是像上面一樣定義一個localPtr,用它在臨界區內與globalPtr 交換(swap()),這樣能保證把對象的銷毀推遲到臨界區之外。練習:在write() 函數中,globalPtr = newPtr; 這一句有可能會在臨界區內銷毀原來globalPtr 指向的Foo 對象,設法將銷毀行為移出臨界區。