新四季網

你好C並發世界(你好C並發世界)

2023-07-27 19:10:23 4

在應用程式中使用並發的原因主要有兩個:關注點分離和性能。事實上,我甚至可以說它們差不多是使用並發的唯一原因;當你觀察得足夠仔細時,一切其他因素都可以歸結到這兩者之一(或者可能是二者兼有,當然,除了像「我願意」這樣的原因之外)。

為了劃分關注點而使用並發

在編寫軟體時,劃分關注點總是個好主意。通過將相關的代碼放在一起並將無關的代碼分開,這種方法可以使你的程序更容易理解和測試,從而減少出錯的可能性。你可以使用並發來分隔不同的功能區域,即使在這些不同功能區域的操作需要在同一時刻發生的情況下。如果不顯式地使用並發,你要麼被迫編寫任務切換框架,要麼在操作中主動地調用不相關的一段代碼。

考慮一類帶有用戶界面的密集處理型應用程式,例如為臺式計算機提供的DVD播放程序。這樣一個應用程式基本上具備兩套職能:它不僅要從光碟中讀取數據,解碼圖像和聲音,並把它們及時輸出至視頻和音頻硬體,從而實現DVD的無錯播放;它還要接受來自用戶的輸入,例如當用戶單擊暫停或返回菜單甚至退出按鍵的情況。在單個線程中,應用程式須在回放期間定期檢查用戶的輸入,於是將DVD回放代碼和用戶界面代碼合在一起。通過使用多線程來分隔這些關注點,用戶界面代碼和DVD回放代碼不再需要如此緊密地交織在一起。一個線程可以處理用戶界面,另一個處理DVD回放,它們之間會有交互,例如用戶點擊暫停,但現在這些交互直接與眼前的任務有關。

這會帶來響應性的錯覺,因為用戶界面線程通常可以立即響應用戶的請求,即使在請求被傳達給工作的線程,響應為簡單地顯示正忙的光標或請等待的消息的情況。類似地,獨立的線程常被用於運行必須在後臺連續運行的任務,例如在桌面搜索程序中監視文件系統的變化。以這種方式使用線程一般會使每個線程的邏輯更加簡單,因為它們之間的交互可以被限制為清晰可辨的點,而不是到處散播不同任務的邏輯。

在這種情況下,線程的數量與CPU可用內核的數量無關,因為對線程的劃分是基於概念上的設計而不是試圖增加吞吐量。

為了性能而使用並發

多處理器系統已經存在了幾十年,但直到最近,他們幾乎只能在超級計算機、大型機和大型伺服器系統中才能看到。然而晶片製造商越來越傾向於多核晶片的設計,即在單個晶片上集成2、4、16或更多的處理器,從而達到比單核心更好的性能。因此,多核臺式計算機,甚至多核嵌入式設備,現在越來越普遍。這些計算機的計算能力的提高不是源自使單一任務運行的更快,而是源自並行運行多個任務。在過去,程式設計師曾坐等他們的程序隨著處理器的更新換代而變得更快,無需他們這邊做出任何努力。但是現在,就像Herb Sutter所說的,「免費的午餐結束了1」。如果軟體想要利用日益增長的計算能力,它必須設計為並發運行多個任務。程式設計師因此必須留意,而且那些迄今都忽略並發的人們必須注意它並將其加入他們的工具箱中。

有兩種方式為了性能使用並發。首先,也是最明顯的,是將一個單個任務分成幾部分且各自並行運行,從而降低總運行時間,這就是任務並行(taskparallelism)。雖然這聽起來很直觀,但它可以是一個相當複雜的過程,因為在各個部分之間可能存在很多的依賴。區別可能是在過程方面——一個線程執行算法的一部分而另一個線程執行算法的另一部分——或是在數據方面——每個線程在不同的數據部分上執行相同的操作。後一種方法被稱為數據並行(dataparallelism)

容易受這種並行影響的算法常被稱為易並行(embarrassinglyparallel)。拋開你可能會尷尬地面對的很容易並行化的代碼這一含義,這是一件好事情。我曾遇到過的關於此算法的別的術語是自然並行(naturallyparallel)便利並發(convenientlyconcurrent)。易並行算法具有良好的可擴展特性——隨著可用硬體線程數量的提升,算法的並行性可以隨之增加與之匹配。這樣的一個算法是諺語「人多力量大」的完美體現。對於非易並行算法的那一部分,你可以將算法劃分為一個固定(因而不可擴展)數量的並行任務。在線程之間劃分任務的技巧涵蓋在第8章中。

使用並發來提升性能的第二種方法是使用可用的並行方式來解決更大的問題。與其同時處理一個文件,不如酌情處理2個或10個或20個。雖然這實際上只是數據並行的一種應用,通過對多組數據同時執行相同的操作,但還是有不同的重點。處理一個數據塊仍然需要同樣的時間,但在相同的時間內卻可以處理更多的數據。當然,這種方法也存在限制,且並非在所有情況下都是有益的,但是這種方法所帶來的吞吐量提升可以讓一些新玩意變得可能。例如,如果圖片的各部分可以並行處理,就能提高視頻處理的解析度。

什麼時候不使用並發

知道何時不使用並發與知道何時要使用它同等重要。基本上,不使用並發的唯一原因就是在收益比不上成本的時候。使用並發的代碼在很多情況下難以理解,因此編寫和維護的多線程代碼就有直接的腦力成本,同時額外的複雜性也可能導致更多的錯誤。除非潛在的性能增益足夠大或關注點分離得足夠清晰,能抵消確保其正確所需的額外的開發時間以及與維護多線程代碼相關的額外成本,否則不要使用並發。

同樣地,性能增益可能不會如預期的那麼大。在啟動線程時存在固有的開銷,因為作業系統必須分配相關的內核資源和堆棧空間,然後將新線程加入調度器中,所有這一切都要佔用時間。如果在線程上運行的任務完成得很快,那麼任務實際上佔據的時間與啟動線程的開銷時間相比就顯得微不足道,可能會導致應用程式的整體性能還不如通過產生線程直接執行該任務。

此外,線程是有限的資源。如果讓太多的線程同時運行,則會消耗作業系統資源,並且使得作業系統整體上運行得更緩慢。不僅如此,運行太多的線程會耗盡進程的可用內存或地址空間,因為每個線程都需要一個獨立的堆棧空間。對於一個可用地址空間限制為4GB的扁平架構的32位進程來說,這尤其是個問題。如果每個線程都有一個1MB的堆棧(對於很多系統來說是典型的),那麼4096個線程將會用盡所有地址空間,不再為代碼、靜態數據或者堆數據留有空間。雖然64位(或者更大)的系統不存在這種直接的地址空間限制,它們仍然只具備有限的資源:如果你運行太多的線程,最終會導致問題。儘管線程池(參見第9章)可以用來限制線程的數量,但這並不是靈丹妙藥,它們也有自己的問題。

如果客戶端/伺服器應用程式的伺服器端為每一個連結啟動一個獨立的線程,對於少量的連結是可以正常工作的,但當同樣的技術用於需要處理大量連結的高需求伺服器時,就會因為啟動太多線程而迅速耗盡系統資源。在這種場景下,謹慎地使用線程池可以提供優化的性能(參見第9章)。

最後,運行越多的線程,作業系統就需要做越多的上下文切換。每個上下文切換都需要耗費本可以花在有價值工作上的時間,所以在某些時候,增加一個額外的線程實際上會降低而不是提高應用程式的整體性能。為此,如果你試圖得到系統的最佳性能,考慮可用的硬體並發(或缺乏之)並調整運行線程的數量是必需的。

為了性能優化而使用並發就像所有其他優化策略一樣,它擁有極大提高應用程式性能的潛力,但它也可能使代碼複雜化,使其更難理解和更容易出錯。因此,只有對應用程式中的那些具有顯著增益潛力的性能關鍵部分才值得這樣做。當然,如果性能收益的潛力僅次於設計清晰或關注點分離,可能也值得使用多線程設計。

假設你已經決定確實要在應用程式中使用並發,無論是為了性能、關注點分離,或是因為「多線程星期一」,對於C 程式設計師來說意味著什麼?

在C 中使用並發和多線程

通過多線程為並發提供標準化的支持對C 來說是新鮮事物。只有在即將到來的C 11標準中,你才能不依賴平臺相關的擴展來編寫多線程代碼。為了理解新版本C 線程庫中眾多規則背後的基本原理,了解其歷史是很重要的。

C 多線程歷程

1998C 標準版不承認線程的存在,並且各種語言要素的操作效果都以順序抽象機的形式編寫。不僅如此,內存模型也沒有被正式定義,所以對於1998 C 標準,你沒辦法在缺少編譯器相關擴展的情況下編寫多線程應用程式。

當然,編譯器供應商可以自由地向語言添加擴展,並且針對多線程的C API的流行——例如在POSIX C和Microsoft Windows API中的那些——導致很多C 編譯器供應商通過各種平臺相關的擴展來支持多線程。這種編譯器支持普遍地受限於只允許使用該平臺相應的C API以及確保該C 運行時庫(例如異常處理機制的代碼)在多線程存在的情況下運行。儘管極少有編譯器供應商提供了一個正式的多線程感知內存模型,但編譯器和處理器的實際表現也已經足夠好,以至於大量的多線程的C 程序已被編寫出來。

由於不滿足於使用平臺相關的C API來處理多線程,C 程式設計師曾期望他們的類庫提供面向對象的多線程工具。像MFC這樣的應用程式框架,以及像Boost和ACE這樣的C 通用類庫曾積累了多套C 類,封裝了下層的平臺相關API並提供高級的多線程工具以簡化任務。各類庫的具體細節,特別是在啟動新線程的方面,存在很大差異,但是這些類的總體構造存在很多共通之處。有一個為許多C 類庫共有的,同時也是為程式設計師提供很大便利的特別重要的設計,就是帶鎖的資源獲得即初始化(RAII, ResourceAcquisitionIsInitialization)的習慣用法,來確保當退出相關作用域的時候互斥元被解鎖。

許多情況下,現有的C 編譯器所提供的多線程支持,例如Boost和ACE,綜合了平臺相關API以及平臺無關類庫的可用性,為編寫多線程C 代碼提供一個堅實的基礎,也因此大約有數百萬行C 代碼作為多線程應用程式的一部分而被編寫出來。但缺乏標準的支持,意味著存在缺少線程感知內存模型從而導致問題的場合,特別是對於那些試圖通過使用處理器硬體能力來獲取更高性能,或是編寫跨平臺代碼,但是在不同平臺之間編譯器的實際表現存在差異。

新標準中的並發支持

所有這些都隨著新的C 11標準的發布而改變了。不僅有了一個全新的線程感知內存模型,C 標準庫也被擴展了,包含了用於管理線程(參見第2章)、保護共享數據(參見第3章)、線程間同步操作(參見第4章)以及低級原子操作(參見第5章)的各個類。

新的C 線程庫很大程度上基於之前通過使用上文提到的C 類庫而積累的經驗。特別地,Boost線程庫被用作新類庫所基於的主要模型,很多類與Boost中的對應者共享命名和結構。在新標準演進的過程中,這是個雙向流動,Boost線程庫也改變了自己,以便在多個方面匹配C 標準,因此從Boost遷移過來的用戶將會發現自己非常適應。

正如本章開篇提到的那樣,對並發的支持僅僅是新C 標準的變化之一,此外還存在很多對於程式語言自身的改善,可以使得程式設計師們的工作更便捷。這些內容雖然不在本書的論述範圍之內,但是其中的一些變化對於線程庫本身及其使用方式已經形成了直接的衝擊。附錄A對這些語言特性做了簡要的介紹。

C 中對原子操作的直接支持,允許程式設計師編寫具有確定語義的高效代碼,而無需平臺相關的彙編語言。這對於那些試圖編寫高效的、可移植代碼的程式設計師們來說是一個真正的福利。不僅有編譯器可以搞定平臺的具體內容,還可以編寫優化器來考慮操作的語義,從而讓程序作為一個整體得到更好的優化。

C 線程庫的效率

對於C 整體以及包含低級工具的C 類——特別是在新版C 線程庫裡的那些,參與高性能計算的開發者常常關注的一點就是效率。如果你正尋求極致的性能,那麼理解與直接使用底層的低級工具相比,使用高級工具所帶來的實現成本,是很重要的。這個成本就是抽象懲罰(abstractionpenalty)

C 標準委員會在整體設計C 標準庫以及專門設計標準C 線程庫的時候,就已經十分注重這一點了。其設計的目標之一就是在提供相同的工具時,通過直接使用低級API就幾乎或完全得不到任何好處。因此該類庫被設計為在大部分平臺上都能高效實現(帶有非常低的抽象懲罰)。

C 標準委員會的另一個目標,是確保C 能提供足夠的低級工具給那些希望與硬體工作得更緊密的程式設計師,以獲取終極性能。為了達到這個目的,伴隨著新的內存模型,出現了一個全面的原子操作庫,用於直接控制單個位、字節、線程間同步以及所有變化的可見性。這些原子類型和相應的操作現在可以在很多地方加以使用,而這些地方以前通常被開發者選擇下放到平臺相關的彙編語言中。使用了新的標準類型和操作的代碼因而具有更佳的可移植性,並且更易於維護。

C 標準庫也提供了更高級別的抽象和工具,它們使得編寫多線程代碼更簡單和不易出錯。有時候運用這些工具確實會帶來性能成本,因為必須執行額外的代碼。但是這種性能成本並不一定意味著更高的抽象懲罰;總體來看,這種性能成本並不比通過手工編寫等效的函數而招致的成本更高,同時編譯器可能會很好地內聯大部分額外的代碼。

在某些情況下,高級工具提供超出特定使用需求的額外功能。在大部分情況下這都不是問題,你沒有為你不使用的那部分買單。在罕見的情況下,這些未使用的功能會影響其他代碼的性能。如果你更看重程序的性能,且代價過高,你可能最好是通過較低級別的工具來手工實現需要的功能。在絕大多數情況下,額外增加的複雜性和出錯的機率遠大於小小的性能提升所帶來的潛在收益。即使有證據確實表明瓶頸出現在C 標準庫的工具中,這也可能歸咎於低劣的應用程式設計而非低劣的類庫實現。例如,如果過多的線程競爭一個互斥元,這將會顯著影響性能。與其試圖在互斥操作上花掉一點點的時間,還不如重新構造應用程式以減少互斥元上的競爭來得划算。設計應用程式以減少競爭會在第8章中加以闡述。

在非常罕見的情況下,C 標準庫不提供所需的性能或行為,這時則有必要運用特定的平臺相關的工具。

平臺相關的工具

雖然C 線程庫為多線程和並發處理提供了頗為全面的工具,但是在所有的平臺上,都會有些額外的平臺相關工具。為了能方便地訪問那些工具而又不用放棄使用標準C 線程庫帶來的好處,C 線程庫中的類型可以提供一個native_handle成員函數,允許通過使用平臺相關API直接操作底層實現。就其本質而言,任何使用native_handle執行的操作是完全依賴於平臺的,這也超出了本書(同時也是標準C 庫本身)的範圍。

當然,在考慮使用平臺相關的工具之前,明白標準庫能夠提供什麼是很重要的,那麼讓我們通過一個例子來開始。

1The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software, Herb Sutter, Dr. Dobb’s Journal, 30(3), March 2005.http://www.gotw.ca/publications/concurrency-ddj.htm

本文摘自《C 並發編程實戰》

《C 並發編程實戰》是一本基於C 11新標準的並發和多線程編程深度指南。內容包括從std::thread、std::mutex、std::future和std::async等基礎類的使用,到內存模型和原子操作、基於鎖和鎖數據結構的構建,再擴展到並行算法、線程管理,最後還介紹了多線程代碼的測試工作。本書的附錄部分還對C 11新語言特性中與多線程相關的項目進行了簡要的介紹,並提供了C 11線程庫的完整參考。

《C 並發編程實戰》適合於需要深入了解C 多線程開發的讀者,以及使用C 進行各類軟體開發的開發人員、測試人員。對於使用第三方線程庫的讀者,也可以從本書後面的章節中了解到相關的指引和技巧。同時,本書還可以作為C 11線程庫的參考工具書。

,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了