關於函數重入和線程安全之我討論(一)

線程安全:一個函數被稱爲線程安全的(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。

可重入函數

可重入函數:可重入函數是線程安全函數的一種,其特點在於它們被多個線程調用時,不會引用任何共享數據。
可重入函數通常要比不可重入的線程安全函數效率高一些,因爲它們不需要同步操作。更進一步說,將第2類線程不安全函數轉化爲線程安全函數的唯一方法就是重寫它,使之可重入。

下面爲rand函數的一個可重入版本

int rand_r(unsigned int* nextp)
{
     *nextp = *nextp * 1103515245 + 12345;
      return (unsigned int) (*nextp / 65536) % 32768;
}

顯式可重入函數:如果所有函數的參數都是傳值傳遞的(沒有指針),並且所有的數據引用都是本地的自動棧變量(也就是說沒有引用靜態或全局變量),那麼函數就是顯示可重入的,也就是說不管如何調用,我們都可斷言它是可重入的。

隱式可重入函數:可重入函數中的一些參數是引用傳遞(使用了指針),也就是說,在調用線程小心地傳遞指向非共享數據的指針時,它纔是可重入的。例如rand_r就是隱式可重入的。
我們使用可重入(reentrant)來包括顯式可重入函數和隱式可重入函數。然而,可重入性有時是調用者和被調用者共有的屬性,並不只是被調用者單獨的屬性。

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