可重入函數與線程安全的區別及聯繫

可重入函數

wKiom1eF3qPDXs6AAAB5lzVuCiE996.png

 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、全局變量等等,不一定導致調用結果不可再現性)。只要使調用結果具有可再現性,那麼該函數就是可重入的。

爲了保證函數是可重入的,需要做到一下幾點:

1,不在函數內部使用靜態或者全局數據

2,不返回靜態或者全局數據,所有的數據都由函數調用者提供

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)。我們定義四類(有相交的)線程不安全函數。

第1類:不保護共享變量的函數

將這類線程不安全函數變爲線程安全的,相對比較容易:利用像P和V操作這樣的同步操作來保護共享變量。這個方法的優點是在調用程序中不需要做任何修改,缺點是同步操作將減慢程序的執行時間。

第2類:保持跨越多個調用的狀態函數

一個僞隨機數生成器是這類不安全函數的簡單例子。

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函數變爲線程安全的唯一方式是重寫它,使得它不再使用任何靜態數據,取而代之地依靠調用者在參數中傳遞狀態信息。這樣的缺點是,程序員現在要被迫改變調用程序的代碼。

第3類:返回指向靜態變量指針的函數

某些函數(如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;}

第4類:調用線程不安全函數的函數

如果函數f調用線程不安全函數g,那麼f就是線程不安全的嗎?不一定。如果g是類2類函數,即依賴於跨越多次調用的狀態,那麼f也是不安全的,而且除了重寫g以外,沒有什麼辦法。然而如果g是第1類或者第3類函數,那麼只要用互斥鎖保護調用位置和任何得到的共享數據,f可能仍然是線程安全的。比如上面的gethostbyname_ts。

線程安全,是針對多線程而言的。那麼和可重入聯繫起來,我們可以斷定,可重入函數必定是線程安全的,但是線程安全的,不一定是可重入的。不可重入函數,函數調用結果不具有可再現性,可以通過互斥鎖等機制,使之能安全的同時被多個線程調用,那麼,這個不可重入函數就是轉換成了線程安全。

線程安全,描述的是函數能同時被多個線程安全的調用,並不要求調用函數的結果具有可再現性。也就是說,多個線程同時調用該函數,允許出現互相影響的情況,這種情況的出現需要某些機制比如互斥鎖來支持,使之安全。

聯繫:可重入函數一定是線程安全的,但線程安全不一定是可重入的。

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