【读书笔记】并发编程需要注意的几个典型问题
? ? ? ? 本文為《Computer Systems: A Programmer's Perspective》第12.7節—并發編程問題的讀書筆記。下面開始正文。
1. 線程安全
? ? ? ? 一個線程安全(thread-safety)的函數應滿足條件:當且僅當被多個并發線程反復調用時,它會一直產生正確的結果。相對地,若一個不是線程安全的函數被稱為線程不安全(thread-unsafety)函數。
? ? ? ? 我們能定義出四個(不相交)的線程不安全函數類:
? ? ? ? 1)不保護共享變量的函數
? ? ? ? 很容易理解為什么這類函數線程不安全,將這類函數改造成線程安全的也相對容易:利用類似于信號量的P/V操作或操作系統支持的其它同步操作來保護共享變量。這種改造方法的優點是在調用這類函數的上層程序中不需要做如何修改,缺點是同步操作會減慢程序的執行時間。
? ? ? ? 2)保持跨越多個調用的狀態的函數
? ? ? ? 例如下面的這段偽隨機數生成器代碼:
?
? ? ? ? 上面的代碼中,srand()是線程不安全的,因為當前調用結果依賴于前次調用的中間結果。當調用srand()為rand()設置好隨機種子后,單線程反復調用rand(),能夠預期得到一個可重復的隨機數字序列。然而,如果多線程調用rand(),這種假設就不再成立了。
? ? ? ? 將類似于rand()這樣的函數改造為線程安全函數的唯一方法是重寫它,使得其不再使用任何static或global數據,而是依靠調用者在參數中傳遞狀態信息。這樣做的缺點是:程序員需要被迫修改調用程序中的代碼,在大型程序中,可能會修改成百上千個調用位置,很容易引起bug。
? ? ? ? 3)返回指向靜態變量的指針的函數
? ? ? ? 某些函數,如ctime和gethostbyname,將計算結果放到一個static變量中,然后返回指向這個變量的指針。如果從并發線程中調用這類函數,將可能引發災難,因為正在被一個線程使用的結果會被另一個線程悄悄覆蓋。
? ? ? ? 有兩種方法來處理這類線程不安全函數。一種選擇是重寫函數,由調用者來傳遞存放結果的變量地址,這消除了所有的共享數據,但需要程序員修改函數的源代碼和上層的調用代碼(很多時候,修改源代碼是不現實的,例如標準庫函數或第三方提供的庫)。第二種選擇是使用加鎖-拷貝(lock-and-copy)技術,基本思想是將線程不安全函數用互斥鎖保護起來,即在每個調用位置,程序的執行路徑為:加互斥鎖->調用線程不安全函數->將函數返回結果拷貝到調用者提供的私有存儲變量中->解互斥鎖。為盡可能減少對調用者的修改,我們可以定義一個線程安全的包裝函數,由它來執行加鎖拷貝,上層調用者通過調用這個包裝函數來取代所有對線程不安全函數的調用,從而實現線程安全。
? ? ? ? 下面給出利用lock-and-copy技術實現的一個線程安全版本的ctime:? ??
? ? ? ? 博主按:上面提到的定義一個與原函數具有相同接口參數和返回值的包裝函數來執行復雜操作的思路,最初是由 Richard Stevens在其經典力作《Unix Network Programming》中引入的,讀過這本經典書籍的同學應該不會感到陌生。只可惜大師意外早逝,令人唏噓啊。
? ? ? ? 4)調用線程不安全函數的函數
? ? ? ? 若函數f調用線程不安全函數g,那么f就是線程不安全的嗎?
? ? ? ? 答案是:不一定。如果g是上面提到的第2類函數,即依賴與跨越多次調用的狀態,則f也是線程不安全的,而且除了重寫g外,沒有什么改造辦法。然而,若g是第1類或第3類函數,那么只有我們用一個互斥鎖來保護調用位置和任何得到的共享數據,f仍然可能是線程安全的。上面給出的利用加速-拷貝技術實現的ctime_ts代碼就是一個例子,在該示例中,我們使用lock-and-copy編寫了一個線程安全函數,它調用了一個線程不安全的函數。
2. 可重入性
? ? ? ? 有一類重要的線程安全函數,叫做可重入函數(reentrant function),它具有如下屬性:當它們被多個線程調用時,不會引用任何共享數據。
? ? ? ? 下圖給出了可重入函數、線程安全函數和線程不安全函數之間的集合關系:
? ? ? ??
? ? ? ? 可重入函數通常要比不可重入的線程安全函數高效,因為它們不需要同步操作。進一步講,將上面提到的第2類函數改造為線程安全函數的唯一方法就是將其重寫為可重入的。下面的代碼展示了這一點,其關鍵思想是用調用者傳遞的指針取代靜態的next變量。? ??
? ? ? ? 檢查某個函數的代碼并先驗地斷定它是可重入的,這可能嗎?
? ? ? ? 答案是:不一定。
? ? ? ? 若所有的函數參數都是值傳遞,且所有的數據引用都是本地自動棧變量,則函數就是顯式可重入的(explicitly reentrant),也即,無論它是被如何調用的,我們都可以斷定它是可重入的。
? ? ? ? 若將顯式可重入函數的某些參數改為指針傳遞,我們就得到了一個隱式可重入(implicitly reentrant)函數,也即,如果調用線程小心地傳遞指向非共享數據的指針,那么它是可重入的。
?
3. 在線程化的程序中使用已存在的庫函數
? ? ? ? 所有定義在標準C庫中的函數都是線程安全的。大多數Unix函數也都是線程安全的,只有一小部分是例外,這些函數如下圖所列。
? ? ? ?
? ? ? ? Unix系統提供大多數線程不安全函數的可重入版本。可重入版本的名字總是以"_r"后綴結尾。例如,gethostbyname的可重入版本是gethostbyname_r。我們應盡可能使用這些函數。
4. 競爭
? ? ? ? 當一個程序的正確性依賴與一個線程要在另一個線程到達y點之前到達它的控制流中的x點時,就會發生競爭(race)。通常發生競爭是因為程序員假定線程將按照某種特殊的軌跡線穿過執行狀態空間,而忘記了另一條準則規定:線程化的程序必須對任何可行的軌跡都正確工作。
? ? ? ? 要理解多線程中的競爭問題,可參考下面的代碼:
? ? ? ? 上面的代碼中,thread routine中打印的tid可能會產生非預期的結果。 問題是由每個對等線程和主線程之間的競爭引起的:主線程通過for循環創建對等線程時,它傳遞了一個指向本地棧變量i的指針。在此時,競爭出現在下次循環體調用pthread_create和thread()函數第1行參數的間接引用和賦值之間。如果對等線程在主線程執行下個循環的pthread_create前就執行了thread()的第1行,那么myid就得到了正確的ID;否則,它的值就是其它線程的ID。 令人驚慌的是,我們是否得到正確的答案依賴于內核是如何調動線程執行的。一種可能的情況是:在某些版本的操作系統中,錯誤的執行結果可能會暴露給程序員,而在另一些系統中,它可能總是能"正確"工作,讓程序員"幸福地"錯覺不到程序的嚴重錯誤。
? ? ? ? 博主按:對新手來說,這個bug非常隱蔽,請務必引起重視。
? ? ? ??下面是消除競爭后的示例代碼:
?
5. 死鎖
? ? ? ? 在并發編程中,死鎖(deadlock)是一個讓程序員頭疼的問題。關于死鎖,操作系統方面的權威專家Andrew S. Tanenbaum在《Modern Operating Systems》一書中用整整一章來介紹,足見死鎖在系統編程方面的重要性。大部分死鎖都與資源相關,本文引用該書中對死鎖的規范定義:
? ? ? ? A set of processes is deadlocked if each process in the set is waiting for an event that only another process in the set can cause. ? ? ? ?
? ? ? ? 中文翻譯:如果一個進程集合中的每個進程都在等待只能由該進程集合中的其它進程才能引發的事件,那么,該進程集合就是死鎖的。
? ? ? ? 程序死鎖有很多原因,要避免死鎖一般而言是很困難的。然而,當使用二元信號量來實現互斥時,避免死鎖的規則變得相對比較簡單:
? ? ? ? 互斥鎖加鎖順序規則:若對于程序中每對互斥鎖(s, t),每個同時占用s和t的線程都按照相同的順序對它們加鎖那么這個程序就是無死鎖的。
? ? ? ? 關于死鎖的更詳細的討論(如資源死鎖的條件,死鎖建模,死鎖檢測等細節)超出了本筆記的范疇。感興趣的同學,建議閱讀《Modern Operating Systems》或其它操作系統教材的相關章節。^_^
【參考資料】
1. <Computer Systems: A Programmer's Perspective>. chapter 12 - Concurrent Programming with Threads
2. mcu cs online material: www.cs.cmu.edu/~kgao/course/15213/notes/week16.ppt?
3. <Modern Operating Systems, 2nd Edition>. chapter 3 - DeadLock
================ EOF ===============
?
轉載于:https://www.cnblogs.com/xinyuyuanm/p/3206234.html
總結
以上是生活随笔為你收集整理的【读书笔记】并发编程需要注意的几个典型问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 敏捷?TOC
- 下一篇: JavaScript高级程序设计读书笔记