可重入函數
node
main函數調用insert函數向一個鏈表head中插入節點node1,插入操做分爲兩步,剛作完 第一步的 時候,由於硬件中斷使進程切換到內核,再次回用戶態以前檢查到有信號待處理,於 是切換 到sighandler函數,sighandler也調用insert函數向同一個鏈表head中插入節 點node2,插入操做的 兩步都作完以後從sighandler返回內核態,再次回到用戶態就從 main函數調用的insert函數中繼續 往下執行,先前作第一步以後被打斷,如今繼續作完第二 步。結果是,main函數和sighandler前後 向鏈表中插入兩個節點,而最後只有一個節點真 正插入鏈表中了。 程序員
insert函數被不一樣的控制流程調用,有可能在第一次調用還沒返回時就再次 進入該函 數,這稱爲重入,insert函數訪問一個全局鏈表,有可能由於重入而形成錯亂,像這樣 的函數稱爲 不可重入函數,反之,若是一個函數只訪問本身的局部變量或參數,則稱爲可重入 (Reentrant) 函數。安全
可重入,並不必定要是多線程的。可重入只關注一個結果可再現性。在APUE中,可函數可重入的概念最早是在講signal的handler的時候提出的。此時進程(線程)正在執行函數fun(),在函數fun()還未執行完的時候,忽然進程接收到一個信號sig, 此時,須要暫停執行fun(),要轉而執行sig信號的處理函數sig_handler(),那麼,若是在sig_handler()中,也剛好調用了函數fun().信號的處理是以軟終端的形式進行的,那麼,當sig_handler()執行完返回以後,CPU會繼續從fun()被打斷的地方往下執行。這裏講的比較特殊,最好的狀況是,進程中調用了fun(),函數,信號處理函數sig_handle()中也調用了fun()。若是fun()函數是可重入的,那麼,屢次調用fun()函數就具備可再現性。從而,兩次調用fun()的結果是正確的預期結果。非可重入函數,則剛好相反。多線程
簡而言之,可重入函數,描述的是函數被屢次調用可是結果具備可再現性。併發
若是fun(),中,使用了static變量、返回全局變量、調用非可重入函數等等,帶有全局性的操做,都將會致使2次以上調用fun()的結果的不可再現性(固然,有些時候使用了static、全局變量等等,不必定致使調用結果不可再現性)。只要使調用結果具備可再現性,那麼該函數就是可重入的。app
爲了保證函數是可重入的,須要作到一下幾點:ide
1,不在函數內部使用靜態或者全局數據函數
2,不返回靜態或者全局數據,全部的數據都由函數調用者提供spa
3,使用本地數據,或者經過製做全局數據的本地拷貝來保護全局數據線程
4, 若是必須訪問全局數據,使用互斥鎖來保護
5,不調用不可重入函數
重入函數的分類
顯式可重入函數:若是全部函數的參數都是傳值傳遞的(沒有指針),而且全部的數據引用都是本地的自動棧變量(也就是說沒有引用靜態或全局變量),那麼函數就是顯示可重入的,也就是說無論如何調用,咱們均可斷言它是可重入的。
隱式可重入函數:可重入函數中的一些參數是引用傳遞(使用了指針),也就是說,在調用線程當心地傳遞指向非共享數據的指針時,它纔是可重入的。例如rand_r就是隱式可重入的。
咱們使用可重入(reentrant)來包括顯式可重入函數和隱式可重入函數。然而,可重入性有時是調用者和被調用者共有的屬性,並不僅是被調用者單獨的屬性。
線程安全
APUE中的描述:If a function can be safely called by multiple threads at the same time, we say that the function is thread-safe
若是一個函數可以安全的同時被多個線程調用而獲得正確的結果,那麼,咱們說這個函數是線程安全的。所謂安全,一切可能致使結果不正確的因素都是不安全的調用。
一個函數被稱爲線程安全的(thread-safe),當且僅當被多個併發進程反覆調用時,它會一直產生正確的結果。若是一個函數不是線程安全的,咱們就說它是線程不安全的(thread-unsafe)。咱們定義四類(有相交的)線程不安全函數。
將這類線程不安全函數變爲線程安全的,相對比較容易:利用像P和V操做這樣的同步操做來保護共享變量。這個方法的優勢是在調用程序中不須要作任何修改,缺點是同步操做將減慢程序的執行時間。
一個僞隨機數生成器是這類不安全函數的簡單例子。
unsigned int next = 1; int rand(void){ next = next * 1103515245 + 12345; return (unsigned int) (next / 65536) % 32768;} void srand(unsigned int seed){ next = seed;}
rand函數是線程不安全的,由於當前調用的結果依賴於前次調用的中間結果。當咱們調用srand爲rand設置了一個種子後,咱們反覆從一個單線程中調用rand,咱們可以預期一個可重複的隨機數字序列。可是,若是有多個線程同時調用rand函數,這樣的假設就不成立了。
使得rand函數變爲線程安全的惟一方式是重寫它,使得它再也不使用任何靜態數據,取而代之地依靠調用者在參數中傳遞狀態信息。這樣的缺點是,程序員如今要被迫改變調用程序的代碼。
某些函數(如gethostbyname)將計算結果放在靜態結構中,並返回一個指向這個結構的指針。若是咱們從併發線程中調用這些函數,那麼將可能發生災難,由於正在被一個線程使用的結果會被另外一個線程悄悄地覆蓋了。
有兩種方法來處理這類線程不安全函數。一種是選擇重寫函數,使得調用者傳遞存放結果的結構地址。這就消除了全部共享數據,可是它要求程序員還要改寫調用者的代碼。
若是線程不安全函數是難以修改或不可修改的(例如,它是從一個庫中連接過來的),那麼另一種選擇就是使用lock-and-copy(加鎖-拷貝)技術。這個概念將線程不安全函數與互斥鎖聯繫起來。在每一個調用位置,對互斥鎖加鎖,調用函數不安全函數,動態地爲結果非配存儲器,拷貝函數返回的結果到這個存儲器位置,而後對互斥鎖解鎖。一個吸引人的變化是定義了一個線程安全的封裝(wrapper)函數,它執行lock-and-copy,而後調用這個封轉函數來取代全部線程不安全的函數。例以下面的gethostbyname的線程安全函數。
struct hostent* gethostbyname_ts(char* host){ struct hostent* shared, * unsharedp; unsharedp = Malloc(sizeof(struct hostent)); P(&mutex) shared = gethostbyname(hostname); *unsharedp = * shared; V(&mutex); return unsharedp;}
若是函數f調用線程不安全函數g,那麼f就是線程不安全的嗎?不必定。若是g是類2類函數,即依賴於跨越屢次調用的狀態,那麼f也是不安全的,並且除了重寫g之外,沒有什麼辦法。然而若是g是第1類或者第3類函數,那麼只要用互斥鎖保護調用位置和任何獲得的共享數據,f可能仍然是線程安全的。好比上面的gethostbyname_ts。
線程安全,是針對多線程而言的。那麼和可重入聯繫起來,咱們能夠判定,可重入函數一定是線程安全的,可是線程安全的,不必定是可重入的。不可重入函數,函數調用結果不具備可再現性,能夠經過互斥鎖等機制,使之能安全的同時被多個線程調用,那麼,這個不可重入函數就是轉換成了線程安全。
線程安全,描述的是函數能同時被多個線程安全的調用,並不要求調用函數的結果具備可再現性。也就是說,多個線程同時調用該函數,容許出現互相影響的狀況,這種狀況的出現須要某些機制好比互斥鎖來支持,使之安全。
聯繫:可重入函數必定是線程安全的,但線程安全不必定是可重入的。