C++的可移植性和跨平臺開發[6]:多線程

最近一個多月寫的帖子比較雜,導致本系列又好久沒更新了。結果又有網友在評論中催我了,搞得我有點囧。今天趕緊把多線程篇補上。上次聊操作系統的時候,由於和OS有關的話題比較瑣碎,雜七雜八說了一大堆。當時一看篇幅有點長,就把多進程和多線程的部分給留到後面了。  

★編譯器  ◇關於C運行庫選項  先來說一個很基本的問題:關於C運行庫(後面簡稱CRT:C Run-Time)的設置。本來不想聊這麼低級的問題,但周圍有好幾個人都在這個地方吃過虧,所以還是講一下。  大部分C++編譯器都會自帶有CRT(可能還不止一個)。某些編譯器自帶的CRT可能會根據線程的支持分爲單線程CRT和多線程CRT兩類。當你要進行多線程開發的時候,別忘了確保相關的C++工程項目使用的是多線程的CRT。否則會死得很難看。  尤其當你使用Visual C++創建工程項目,更加要小心。如果新建的工程項目是不含MFC的(包括Console工程和Win32工程),那工程的默認設置會是使用“單線程CRT”,如下圖所示:
  ◇關於優化選項  “優化選項”是另一個很關鍵的編譯器相關話題。有些編譯器提供號稱很牛X的優化選項,但是某些優化選項可能會有潛在的風險。編譯器可能自作主張打亂執行指令的順序,從而導致出乎意料的線程競態問題(Race Condition,詳細解釋看“這裏”)。劉未鵬同學在“C++多線程內存模型”裏舉了幾個典型的例子,大夥兒可以去瞧一瞧。  建議只使用編譯器常規的速度優化選項即可。其它那些花哨的優化選項,增加的效果未必明顯,但是潛在的風險不小。實在不值得冒險。  以GCC爲例:建議用-O2選項即可(其實-O2是一堆選項的集合),沒必要冒險用-O3(除非你有很充足的理由)。除了-O2和-O3之外,GCC還有一大坨(估計有上百個)其它的優化選項。如果你企圖用當中的某個選項,一定要先把它的特性、可能的副作用都摸清楚,否則將來死都不知道怎麼死的。  

★線程庫的選擇  由於當前的C++ 03標準幾乎沒有涉及線程相關的內容(即使將來C++ 0x包含了線程的標準庫,編譯器廠商的支持在短期內也未必全面),所以在未來很長的一段時間,跨平臺的多線程支持還是要依賴第三方庫。所以線程庫的選擇是大大滴重要。下面大致介紹一下幾個知名的跨平臺線程庫。  ◇ACE  先說一下ACE這個歷史悠久的庫。如果你之前從未接觸過它,先看“這裏”掃盲。從ACE的全稱(Adaptive Communication Environment)來看,它應該是以“通訊”爲主業。不過ACE對“多線程”這個副業的支持還是非常全面的,比如互斥鎖(ACE_Mutex)、條件變量(ACE_Condition)、信號量(ACE_Semaphore)、柵欄(ACE_Barrier)、原子操作(ACE_Atomic_Op)等等。對某些類型比如ACE_Mutex還細分爲線程讀寫鎖(ACE_RW_Thread_Mutex)、線程遞歸鎖(ACE_Recursive_Thread_Mutex)等等。  除了支持很全面,ACE還有另一個很明顯的優點,就是對各種操作系統平臺及其自帶的編譯器支持很好。包括一些老式的編譯器(比如VC6),它也能夠支持(此處所說的支持,不光是能編譯通過,而且要能穩定運行)。這個優點對於跨平臺開發那是相當相當滴明顯。  那缺點捏?由於ACE開工的年頭很早(大概是上世紀九十年代中期),那會兒很多C++的老特性都還沒出來(更別提新特性了),所以感覺ACE整個的風格比較老氣,遠不如boost那麼時髦前衛。  ◇boost::thread  boost::thread正好和ACE形成鮮明對照。這玩意貌似從boost1.32版本開始引入,年頭比ACE短。不過得益於boost裏一幫大牛的支持,發展還是蠻快的。到目前的boost1.38版本,也能夠支持許多特性了(不過似乎沒ACE多)。鑑於很多C++標準委員會的成員雲集在boost社區中,隨着時間的推移,boost::thread終將成爲C++線程的明日之星,前途無量啊!  boost::thread的缺點就是支持的編譯器不夠多,尤其是一些老式編譯器(很多boost的子庫都有此問題,多半因爲用了一些高級的模板語法)。這對於跨平臺而言一個比較明顯的問題。  ◇wxWidgets和QT  wxWidgets和QT都是GUI界面庫,但是它們也都內置和對線程的支持。wxWidgets線程的簡介可以看“這裏”,關於QT線程的簡介可以看“這裏”。這兩個庫對線程的支持差不多,都提供了諸如mutex、condition、semaphore等常用的機制。不過特性沒有ACE豐富。  ◇如何權衡  對於開發GUI軟件並已經用上了wxWidgets或者QT,那你可以直接用它們內置的線程庫(前提是你只用到基本的線程功能)。由於它們內置的線程庫,特性稍嫌單薄。萬一你需要某高級的線程功能,那得考慮替換成boost::thread或ACE。  至於boost::thread和ACE的取捨,主要得看軟件的需求了。如果你要支持的平臺挺多挺雜,那建議選用ACE,以免碰上編譯器不支持的問題。如果你只需要支持少數幾個主流的平臺(比如Windows、Linux、Mac),那建議用boost::thread。畢竟主流操作系統上的編譯器,對boost的支持還是蠻好的。  

★編程上的注意事項  其實多線程開發,需要注意的地方挺多的,我只能大致列幾個印象比較深的注意事項。  ◇關於volatile  說到多線程編程可能碰到的陷阱,那就不得不提到volatile關鍵字。如果你對它還不甚瞭解,先看“這裏”掃盲一下。由於C++ 98和C++ 03標準都沒有定義多線程的內存模型,而標準中也就volatile和線程沾點兒邊。結果導致C++社區中有相當多的口水都集中在volatile身上(其中有不少C++大牛的口水)。有鑑於此,我這裏就不再多囉嗦了。推薦幾個大牛的文章:Andrei Alexandrescu的文章“這裏”、還有Hans Boehm的文章“這裏”和“這裏”。大夥兒自個兒去拜讀一下。  ◇關於原子操作  有些同學光知道多個線程的競爭寫需要加鎖,卻不知道多個讀單個寫也需要保護。比如有某個整數int nCount = 0x01020304;在併發狀態下,一個寫線程去修改它的值nCount = 0x05060708;另一個讀線程去獲取該值。那麼讀線程有沒有可能讀取到一個“壞”的(比如0x05060304)數據捏?  數據是否壞掉,取決於對nCount的讀和寫是否屬於原子操作。而這就依賴於很多硬件相關的因素了(包括CPU的類型、CPU的字長、內存對齊的字節數等)。在某些情況下,確實可能出現數據壞掉。  由於我們討論的是跨平臺的開發,天曉得將來你的代碼會在啥樣的硬件環境下執行。所以在處理類似問題的時候,還是要用第三方庫提供的原子操作類/函數(比如ACE的Atomic_Op)來確保安全。  ◇關於對象的析構  在之前的系列帖子“C++對象是怎麼死的?”裏面,已經分別介紹了Win32平臺和Posix平臺下線程的非自然死亡問題。由於上述幾個跨平臺的線程庫底層還是要調用操作系統自帶的線程API,所以大夥兒還是要盡最大努力確保所有線程都能夠自然死亡。  今天的話題就聊到這裏,下一次聊多進程的話題。

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