對STL容器線程安全性的期待現實一些

轉自:http://www.cnblogs.com/juncheng/articles/1600728.html

標準C++的世界是相當保守和陳舊的。在這個純潔的世界,所有可執行文件都是靜 態鏈接的。

不存在內存映射文件和共享內存。沒有窗口系統,沒有網絡,沒有數據庫,沒有其他進程。

在這種情況下,當發現標準沒有提到任何關於線程的東西時你 不該感到驚訝。你對STL的線程安全有的第一個想法應該是它將因實現而不同。

當然,多線程程序是很普遍的,所以大部分STL廠商努力使他們的實現在線程環境中可以正常工作。

但是,即使他們做得很好,大部分負擔仍在你肩上,而理解爲什麼會這樣是很重要的。

STL廠商只能爲你做一些可以減少你多線程的痛苦的事情,你需要知道他們做了什麼。

在STL容器(和大多數廠商的願望)裏對多線程支持的黃金規則已經由SGI定義,並且在它們的STL網站[21]上發佈。

大體上說,你能從實現裏確定的最多是下列內容:

  • 多個讀取者是安全的。多線程可能同時讀取一個容器的內容,這將正確地執行。當然,在讀取時不能有任何寫入者操作這個容器。
  • 對不同容器的多個寫入者是安全的。多線程可以同時寫不同的容器。

就這些了,那麼讓我解釋你可以期望的是什麼,而不是你可以確定的。有些實現提供這些保證,但是有些不。

寫多線程的代碼很難,很多程序員希望STL實現是完全線程安全的。如果是那樣,程序員可以不再需要自己做並行控制。

毫無疑問這將帶來很多方便,但這也非常難實現。一個庫可能試圖以下列方式實現這樣完全線程安全的容器:

  • 在每次調用容器的成員函數期間都要鎖定該容器。
  • 在每個容器返回的迭代器(例如通過調用begin或end)的生存期之內都要鎖定該容器。
  • 在每個在容器上調用的算法執行期間鎖定該容器。(這事實上沒有意義,因爲,正如條款32所解釋的,算法沒有辦法識別出它們正在操作的容器。
  • 不過,我們將在這裏檢驗這個選項,因爲它的教育意義在於看看爲什麼即使是可能的它也不能工作。)

現在考慮下列代碼。它搜尋一個vector<int>中第一次出現5這個值的地方,而且,如果它找到了,就把這個值改爲0。

vector<int> v;
vector<int>::iterator first5(find(v.begin(), v.end(), 5)); // 行1
if (first5 != v.end()){     // 行2
 *first5 = 0;     // 行3
}

在多線程環境裏,另一個線程可能在行1完成之後立刻修改v中的數據。

如果是那樣,行2對first5和v.end的檢測將是無意義的,因爲v的值可 能和它們在行1結束時的值不同。

實際上,這樣的檢測會產生未定義的結果,因爲另一線程可能插在行1和行2之間,

使first5失效,或許通過進行一次插入 操作造成vector重新分配它的內在內存。

(那將使vector全部的迭代器失效。關於重新分配行爲的細節,參見條款14。)類似的,行3中對*first5的賦值是不安全的,

因爲另一個線程可能在行2和行3之間執行,並以某種方式使first5失效,可能通過刪除它指向(或至少曾經指向)的元素。

在上面列舉的鎖定方法都不能防止這些問題。行1中begin和end調用都返回得很快,以至於不能提供任何幫助,

它們產生的迭代器只持續到這行的結束,而且find也在那行返回。

要讓上面的代碼成爲線程安全的,v必須從行1到行3保持鎖定,很難想象STL實現怎麼能自動推斷出這個。

記住同步原語(例如,信號燈,互斥量,等 等)通常開銷很大,

更難想象實現怎麼在程序沒有明顯性能損失的情況下做到前面所說的——以這樣的一種方式設計——讓最多一個線程在1-3行的過程中能訪問 v。

這樣的考慮解釋了爲什麼你不能期望任何STL實現讓你的線程悲痛消失。取而代之的是,你必須手工對付這些情況中的同步控制。

 在這個例子裏,你可以像這樣做:

vector<int> v;
...
getMutexFor(v);
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end()) {      // 這裏現在安全了
 *first5 = 0;      // 這裏也是
}
releaseMutexFor(v);

一個更面向對象的解決方案是創建一個Lock類,在它的構造函數裏獲得互斥量並在它的析構函數裏釋放它,

這樣使getMutexFor和releaseMutexFor的調用不匹配的機會減到最小。這樣的一個類(其實是一個類模板)基本是這樣的:

template<typename Container>    // 獲取和釋放容器的互斥量
class Lock {      // 的類的模板核心;
public:       // 忽略了很多細節
 Lock(const Containers container)
   : c(container)
 {
  getMutexFor(c);    // 在構造函數獲取互斥量
 }

 ~Lock()
 {
  releaseMutexFor(c);   // 在析構函數裏釋放它
 }

private:
 const Container& c;
};

使用一個類(像Lock)來管理資源的生存期(例如互斥量)的辦法通常稱爲資源獲得即初始化

你應該能在任何全面的C++教材裏讀到它。一個好的開端是Stroustrup的《The C++ Programming Language》[7], 

因爲Stroustrup普及了這個慣用法,但你也可以轉到《More Effective C++》的條款9。不管你參考了什麼來源,

記住上述Lock是被剝離到最原始的本質的。一個工業強度的版本需要很多改進,但是那樣的擴充與STL無關。

而 且這個最小化的Lock已經足夠看出我們可以怎麼把它用於我們一直考慮的例子:

vector<int> v;
...
{        // 建立新塊;
 Lock<vector<int> > lock(v);     // 獲取互斥量
 vector<int>::iterator first5(find(v.begin(), v.end(), 5));
 if (first5 != v.end()) {
  *first5 = 0;
 }
}        // 關閉塊,自動
        // 釋放互斥量

因爲Lock對象在Lock的析構函數裏釋放容器的的互斥量,所以在互斥量需要釋放是就銷燬Lock是很重要的。

爲了讓這件事發生,我們建立一個裏 面定義了Lock的新塊,而且當我們不再需要互斥量時就關閉那個塊。

這聽上去像我們只是用關閉新塊的需要換取了調用releaseMutexFor的需 要,但是這是錯誤的評價。

如果我們忘記爲Lock建立一個新塊,互斥量一樣會釋放,但是它可能發生得比它應該的更晚——當控制到達封閉塊的末端。

如果我們 忘記調用releaseMutexFor,我們將不會釋放互斥量。

而且,這種基於Lock的方法在有異常的情況下是穩健的。

C++保證如果拋出了異常,局部對象就會被銷燬,所以即使當我們正在使用Lock對象時有 異常拋出,Lock也將釋放它的互斥量。

如果我們依賴手工調用getMutexFor和releaseMutexFor,那麼在調用 getMutexFor之後releaseMutexFor之前如果有異常拋出,

我們將不會釋放互斥量。

異常和資源管理是重要的,但是它們不是本條款的主題。

本條款是關於STL裏的線程安全。當涉及到線程安全和STL容器時,你可以確定庫實現允許在一個容器上的多讀取者和不同容器上的多寫入者。

不能希望庫消除對手工並行控制的需要,而且你完全不能依賴於任何線程支持。


發佈了2 篇原創文章 · 獲贊 6 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章