(转)编写可重入和线程安全的代码(Writing Reentrant and Thread-Safe Code)
Writing Reentrant and Thread-Safe Code
原文地址: http:/unet.univie.ac.at/aix/aixprggd/genprogc/writing_reentrant_thread_safe_code.htm
譯者:Love. Katherine,2007-03-28
譯文地址:http://blog.csdn.net/lovekatherine/archive/2007/03/28/1544585.aspx
?
轉載時務必以超鏈接形式標明文章原始出處及作者、譯者信息。
在單線程程序中,只有單一控制流,程序所執行的代碼不必是可重入或線程安全的。在多線程程序中,同一函數和同一資源有可能被多個控制流并發訪問。為了保證資源的完整性,多線程程序中所使用的代碼必須是可重入和線程安全的。
本節提供了編寫可重入和線程安全程序的相關信息。然而本節的主題并不是如何編寫高效并行化的多線程程序,這只有在程序設計階段才能完成。現有的單線程程序必須徹底的重新設計和重新編寫,才能實現高效線程化。
?
理解可重入與線程安全
可重入與線程安全這兩個概念,都與函數處理資源的方式有關。可重入與線程安全是兩個獨立的概念,一個函數可以是可重入或是線程安全,或是同時滿足兩者,或是同時不滿足兩者的。
可重入
一個可重入的函數在執行中并不使用靜態數據,也不返回指向靜態數據的指針。所有使用到的數據都由函數的調用者提供。可重入函數在函數體內不能調用非可重入函數。
一個非可重入函數通常(盡管不是所有情況下)由它的外部接口和使用方法即可進行判斷。例如 strtok()是非可重入的,因為它在內部存儲了被標記分割的字符串;ctime()函數也是非可重入的,它返回一個指向靜態數據的指針,而該靜態數據在每次調用中都被覆蓋重寫。
線程安全
一個線程安全的函數通過加鎖的方式來實現多線程對共享數據的安全訪問。線程安全這個概念,只與函數的內部實現有關,而不影響函數的外部接口。
在C語言中,局部變量是在棧上分配的。因此,任何未使用靜態數據或其他共享資源的函數都是線程安全的。例如,下面的函數是線程安全的:
/* thread-safe function */
int diff(int x, int y)
{
??????? int delta;
??????? delta = y - x;
??????? if (delta < 0)
??????????????? delta = -delta;
??????? return delta;
}
使用全局變量(的函數)是非線程安全的。這樣的信息應該以線程為單位進行存儲,這樣對數據的訪問就可以串行化。一個線程可能會讀取由另外一個線程生成的錯誤代碼。在AIX中,每個線程有獨立的errno變量。
函數可重入化
在多數情況下,非可重入的函數必須被修改過的具有可重入接口的函數所替代。非可重入函數不可用于多線程環境。此外,一個非可重入的函數可能無法滿足線程安全的要求。
?????? 返回數據
很多非可重入函數返回指向靜態數據的指針。可以以兩種方式避免這種情況:
?? * 返回指向動態分配空間的指針。在這種情況下,由調用者負責釋放資源。這種方式的有點在于函數的外部接口不用修改。然后,卻無法保證代碼的向后兼容:調用修改后函數的單線程程序,如果不做修改的話來釋放資源的話,會出現內存泄露的問題。
??? * 使用由調用提供的存儲空間。盡管函數的外部接口需要改動,但是該方法是被推薦的。???
例如,將字符串大寫化的strtoupper()函數,實現如下:
/* non-reentrant function */
char *strtoupper(char *string)
{
??????? static char buffer[MAX_STRING_SIZE];
??????? int index;
?
??????? for (index = 0; string[index]; index++)
??????????????? buffer[index] = toupper(string[index]);
??????? buffer[index] = 0
?
??????? return buffer;
}
上面的函數是非可重入(也是非線程安全的)。運用之前介紹的第一種方法將函數改寫為可重入函數,代碼如下:
/* reentrant function (a poor solution) */
char *strtoupper(char *string)
{
??????? char *buffer;
??????? int index;
??????? /* error-checking should be performed! */
??????? buffer = malloc(MAX_STRING_SIZE);
?
??????? for (index = 0; string[index]; index++)
??????????????? buffer[index] = toupper(string[index]);
??????? buffer[index] = 0
?
??????? return buffer;
}
更佳的改寫方式是改變函數的外部接口。調用者必須為輸入和輸出字符串提供存儲空間,代碼如下:
/* reentrant function (a better solution) */
char *strtoupper_r(char *in_str, char *out_str)
{
??????? int index;
??????? for (index = 0; in_str[index]; index++)
??????? out_str[index] = toupper(in_str[index]);
??????? out_str[index] = 0
??????? return out_str;
}
非可重入的C標準庫是按照第二種方法改寫的。這一點會在后文提到。
?????? 在連續的調用之間(由函數)保存信息
在連續的函數調用之間,不應該由函數保存任何信息,因為多個線程可能一個接一個的調用該函數。如果一個函數需要在連續的調用中保存某個信息,例如工作緩存區或是指針,這個信息應該由調用者負責保存。
考慮下面的例子。lowercase_c函數在連續調用中返回字符串中字符的小寫字符。與strtok()函數的使用方法類似,該字符串只在函數第一次調用時作為參數提供。函數在到達字符串尾部時返回制為0。函數的實現代碼如下:
/* non-reentrant function */
char lowercase_c(char *string)
{
??????? static char *buffer;
??????? static int index;
??????? char c = 0;
?
??????? /* stores the string on first call */
??????? if (string != NULL) {
??????????????? buffer = string;
??????????????? index = 0;
??????? }
??????? /* searches a lowercase character */
??????? for (; c = buffer[index]; index++) {
??????????????? if (islower(c)) {
??????????????????????? index++;
????? ??????????????????break;
??????????????? }
??????? }
??????? return c;
}
該函數是非可重入的。為了將其改寫為可重入函數,由函數的靜態變量index所保存的信息,應該改為由調用者負責保存。函數的可重入版本實現如下:
/* reentrant function */
char reentrant_lowercase_c(char *string, int *p_index)
{
??????? char c = 0;
?
??????? /* no initialization - the caller should have done it */
?
??????? /* searches a lowercase character */
??????? for (; c = string[*p_index]; (*p_index)++) {
??????????????? if (islower(c)) {
??????????????????????? (*p_index)++;
??????????????????????? break;
????????????????? }
??????? }
??????? return c;
}
函數的外部接口和使用方法都需要修改。調用者必須在每次調用函數時提供字符串參數,并且在第一次調用前將index變量初始化為0,正如以下代碼所展示的:
char *my_string;
char my_char;
int my_index;
...
my_index = 0;
while (my_char = reentrant_lowercase_c(my_string, &my_index)) {
??????? ...
}
函數線程安全化
在多線程程序中,所有被多個線程調用的函數都要求是線程安全的。然而,有一種方法能夠實現在多線程程序中調用非線程安全的函數。同樣需要注意的是,非可重入的函數通常也是非線程安全的,然而將其改寫為可重入后,同時也就變為線程安全的了。
?????? 為共享資源加鎖
使用靜態數據或其他共享資源(如文件、終端)的函數,必須通過加鎖的方式來將對資源的訪問串行化來實現線程安全。例如,下面的函數是非線程安全的。
/* thread-unsafe function */
int increment_counter()
{
??????? static int counter = 0;
?
??????? counter++;
??????? return counter;
}
為了實現線程安全,需要用一個靜態鎖來限制對靜態變量counter的訪問,如下面的代碼所示(偽代碼)
/* pseudo-code thread-safe function */
int increment_counter();
{
??????? static int counter = 0;
??????? static lock_type counter_lock = LOCK_INITIALIZER;
?
??????? lock(counter_lock);
??????? counter++;
??????? unlock(counter_lock);
??????? return counter;
}
在使用線程庫的多線程應用程序中,應該是用互斥鎖來實現共享資源訪問的串行化。獨立的庫有可能在線程之外的上下文環境中工作,因此,需要使用其他類型的鎖。
使用非線程安全函數的解決方法
通 過某種解決方法,非線程安全函數是可以被多個線程調用的。這在某些情況下或許是有用的,特別是當在多線程程序中使用一個非線程安全函數庫的時候——或者是 出于測試的目的,或者是由于沒有相應的線程安全版本可用。這種解決方法會增加開銷,因為它需要將對某個或一組函數的調用進行串行化。
?? * 使用作用于整個函數庫的鎖,在每次使用該函數庫(調用庫中的某個函數或是訪問庫中的全局變量)時加鎖,如下面的偽代碼所示:
????? /* this is pseudo-code! */
??????
????? lock(library_lock);
????? library_call();
????? unlock(library_lock);
??????
????? lock(library_lock);
????? x = library_var;
????? unlock(library_lock);
??? 該解決方法有可能會造成性能瓶頸,因為在任意時刻,只有一個線程能任意的訪問或是用該庫。只有在該庫很少被使用的情況下,或是作為一種快速的實現方式,該方法才是可接受的。
??? * 使用作用于單個庫組件(函數或是全局變量)或是一組組件的鎖,如下面的偽代碼所示:
????? /* this is pseudo-code! */
????? lock(library_moduleA_lock);
????? library_moduleA_call();
????? unlock(library_moduleA_lock);
??????
????? lock(library_moduleB_lock);
????? x = library_moduleB_var;
????? unlock(library_moduleB_lock);
?
????? 這種方法與前者相比要復雜一些,但是能提高性能。
由于該類解決方式只應該在應用程序而不是函數庫中使用,可以使用互斥鎖(mutex)來為整個庫加鎖。
可重入和線程安全函數庫
可重入和線程安全函數庫,不僅在多線程環境,在并行以及異步編程的廣泛領域中也是很有用的。因此,堅持使用和編寫可重入和線程安全函數是一個很好的編程習慣。
使用函數庫
AIX base OS附帶函數庫中有幾個是線程安全的。目前的AIX版本中,以下函數庫是線程安全的:
??? * C標準函數庫
??? * 與BSD兼容的函數庫
某些C標準庫函數是非可重入的,例如ctime()和strtok()。這些函數的對應可重入版本的名字為原函數加_r后綴
在編寫多線程程序時,應該使用可重入版本的庫函數替代原始版本。例如,下面的代碼:
token[0] = strtok(string, separators);
i = 0;
do {
??????? i++;
??????? token[i] = strtok(NULL, separators);
} while (token[i] != NULL);
?
在一個多線程程序中應該替換成下面的代碼:
char *pointer;
...
token[0] = strtok_r(string, separators, &pointer);
i = 0;
do {
??????? i++;
??????? token[i] = strtok_r(NULL, separators, &pointer);
} while (token[i] != NULL);
?
非線程安全的函數庫在程序中可以僅由一個線程使用。程序員必須保證使用該函數的線程的唯一性;否則,程序將會執行未期待的行為,甚至崩潰。
改寫函數庫
下面強調了將現存函數庫改寫為可重入和線程安全版本的主要步驟,只適用于C語言的函數庫。
??? * 識別出由函數庫導出的所有全局變量。這些全局變量通常是在頭文件中由export關鍵字定義的。
????? 導出的全局變量應該被封裝起來。每個變量應該被設為函數庫所私有的(通過static關鍵字實現),然后創建全局變量的訪問函數來執行對全局變量的訪問。
??? * 識別出所有靜態變量和其他共享資源。靜態變量通常是由static關鍵字定義的。
?????? ? 每個共享資源都應該與一個鎖關聯起來,鎖的粒度(也就是鎖的數量),影響著函數庫的性能。為了初始化所有鎖,可能需要一個僅被調用一次的初始化函數。
* 識別所有非可重入函數,并將其轉化為可重入。參見函數可重入化
??? * 識別所有非線程安全函數,并將其轉化為線程安全。參見函數線程安全化。
線程安全函數
? 概念:
?????? 線程安全的概念比較直觀。一般說來,一個函數被稱為線程安全的,當且僅當被多個并發線程反復調用時,它會一直產生正確的結果。
? 確保線程安全:
?????? 要確保函數線程安全,主要需要考慮的是線程之間的共享變量。屬于同一進程的不同線程會共享進程內存空間中的全局區和堆,而私有的線程空間則主要包括棧和寄 存器。因此,對于同一進程的不同線程來說,每個線程的局部變量都是私有的,而全局變量、局部靜態變量、分配于堆的變量都是共享的。在對這些共享變量進行訪 問時,如果要保證線程安全,則必須通過加鎖的方式。
? 線程不安全的后果:
?????? 線程不安全可能導致的后果是顯而易見的——共享變量的值由于不同線程的訪問,可能發生不可預料的變化,進而導致程序的錯誤,甚至崩潰。
可重入函數
? 概念:
?????? 可重入的概念基本沒有比較正式的完整解釋,多數的文檔都只是說明什么樣的情況才能保證函數可重入,但沒有完整定義。按照Wiki上的說法,“A computer program or routine is described as reentrant if it can be safely executed concurrently; that is, the routine can be re-entered while it is already running.”根據筆者的經驗,所謂“重入”,常見的情況是,程序執行到某個函數foo()時,收到信號,于是暫停目前正在執行的函數,轉到信號處理 函數,而這個信號處理函數的執行過程中,又恰恰也會進入到剛剛執行的函數foo(),這樣便發生了所謂的重入。此時如果foo()能夠正確的運行,而且處 理完成后,之前暫停的foo()也能夠正確運行,則說明它是可重入的。
? 確保可重入:
?????? 要確保函數可重入,需滿足以下幾個條件:
?????? 1、不在函數內部使用靜態或全局數據
?????? 2、不返回靜態或全局數據,所有數據都由函數的調用者提供。
?????? 3、使用本地數據,或者通過制作全局數據的本地拷貝來保護全局數據。
?????? 4、不調用不可重入函數。
? 不可重入的后果:
?????? 不可重入的后果主要體現在象信號處理函數這樣需要重入的情況中。如果信號處理函數中使用了不可重入的函數,則可能導致程序的錯誤甚至崩潰。
可重入與線程安全
???? 首先,可重入和線程安全是兩個并不等同的概念,一個函數可以是可重入的,也可以是線程安全的,可以兩者均滿足,可以兩者皆不滿組(該描述嚴格的說存在漏洞,參見第二條)。
??? 其次,從集合和邏輯的角度看,可重入是線程安全的子集,可重入是線程安全的充分非必要條件。可重入的函數一定是線程安全的,然過來則不成立。
??? 第三,POSIX 中對可重入和線程安全這兩個概念的定義:
Reentrant Function:
??? A function whose effect, when called by two or more threads,is guaranteed to be as if the threads each executed thefunction one after another in an undefined order, even ifthe actual execution is interleaved.
??????????????????????????????????????????????????????????????????????????????????????????????????????? From IEEE Std 1003.1-2001 (POSIX 1003.1)
????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????? -- Base Definitions, Issue 6
Thread-Safe Function:
??? A function that may be safely invoked concurrently by multiple threads.
?? 另外還有一個 Async-Signal-Safe的概念
Async-Signal-Safe Function:
??? A function that may be invoked, without restriction fromsignal-catching functions. No function is async-signal -safe unless explicitly described as such.
??? 以上三者的關系為:
Reentrant Function 必然是Thread-Safe Function和Async-Signal-Safe Function
可 重入與線程安全的區別體現在能否在signal處理函數中被調用的問題上,可重入函數在signal處理函數中可以被安全調用,因此同時也是Async- Signal-Safe Function;而線程安全函數不保證可以在signal處理函數中被安全調用,如果通過設置信號阻塞集合等方法保證一個非可重入函數不被信號中斷,那 么它也是Async-Signal-Safe Function。
???? 值得一提的是POSIX 1003.1的System Interface缺省是Thread-Safe的,但不是Async-Signal-Safe的。Async-Signal-Safe的需要明確表示,比如fork ()和signal()。
最后讓我們來構想一個線程安全但不可重入的函數:
?? 假設函數func()在執行過程中需要訪問某個共享資源,因此為了實現線程安全,在使用該資源前加鎖,在不需要資源解鎖。
?? 假設該函數在某次執行過程中,在已經獲得資源鎖之后,有異步信號發生,程序的執行流轉交給對應的信號處理函數;再假設在該信號處理函數中也需要調用函數 func(),那么func()在這次執行中仍會在訪問共享資源前試圖獲得資源鎖,然而我們知道前一個func()實例已然獲得該鎖,因此信號處理函數阻 塞——另一方面,信號處理函數結束前被信號中斷的線程是無法恢復執行的,當然也沒有釋放資源的機會,這樣就出現了線程和信號處理函數之間的死鎖局面。
??? 因此,func()盡管通過加鎖的方式能保證線程安全,但是由于函數體對共享資源的訪問,因此是非可重入。
?????? - 如果一個函數中用到了全局或靜態變量,那么它不是線程安全的,也不是可重入的;
?????? - 如果我們對它加以改進,在訪問全局或靜態變量時使用互斥量或信號量等方式加鎖,則可以使它變成線程安全的,但此時它仍然是不可重入的,因為通常加鎖方式是針對不同線程的訪問,而對同一線程可能出現問題;
?????? - 如果將函數中的全局或靜態變量去掉,改成函數參數等其他形式,則有可能使函數變成既線程安全,又可重入。
?????? 比如:strtok函數是既不可重入的,也不是線程安全的;加鎖的strtok不是可重入的,但線程安全;而strtok_r既是可重入的,也是線程安全的。
轉載于:https://www.cnblogs.com/yysblog/archive/2012/06/15/2550754.html
總結
以上是生活随笔為你收集整理的(转)编写可重入和线程安全的代码(Writing Reentrant and Thread-Safe Code)的全部內容,希望文章能夠幫你解決所遇到的問題。