編寫可重入和線程安全的代碼
單線程的進程中僅有一個控制流。這種進程執行的代碼無需可重入或線程安全。在多線程的程序中,同一函數或資源可能被多個控制流並發訪問。為保護資源完整性,多線程程序編碼必須可重入且線程安全。
本節提供了一些編寫可重入和線程安全程序的(指導)信息,但不包括編寫線程高效程序的主題。線程高效程序是高效並行化的程序,僅可在程序設計中實現。現有的單線程程序可變得線程高效,但這需要完全地重新設計和重寫。
理解可重入性和線程安全性
可重入和線程安全與函數處理資源的方式有關。可重入和線程安全是兩個相互獨立的概念:一個函數可以僅是可重入的,可以僅是線程安全的,可以兩者皆是或兩者皆不是。
可重入性
可重入函數不能為后續的調用保持靜態(或全局)數據,也不能返回指向靜態(或全局)數據的指針。函數中用到的所有數據,都應由函數調用者提供(不包括棧上的局部數據)。可重入函數不能調用不可重入的函數。
不可重入的函數經常(但不總是)可以通過其外部接口和用法識別。例如strtok子例程是不可重入的,因為它保存着將被分隔為子串的字符串。ctime也是不可重入的,它返回一個指向靜態數據的指針,每次調用都會覆蓋這些數據。
線程安全性
線程安全的函數通過“鎖”來保護共享資源不被並發地訪問。“線程安全”僅關心函數的實現,而不影響其外部接口。
在C中,局部變量在棧上動態分配,因此,任何不使用靜態數據和其它共享資源的函數就是最普通的線程安全(函數)。例如,以下函數就是線程安全的:

1 /* thread-safe function */ 2 int diff(int x, int y) 3 { 4 int delta; 5 delta = y - x; 6 if (delta < 0) 7 delta = -delta; 8 return delta; 9 }
使用全局數據是線程不安全的。應為每個線程維護一份全局數據的拷貝或封裝全局數據,以使對它的訪問變成串行的。線程可能讀取另一線程造成的錯誤對應的錯誤碼。在AIX系統中,每個線程擁有屬於自己的錯誤碼(errno)值。
編寫可重入函數
在大部分情況下,不可重入的函數修改為可重入函數時,必須修改函數的對外接口。不可重入的函數不能用於多線程。此外,也許不可能讓某個不可重入的函數是線程安全的。
返回數據
很多不可重入的函數返回一個指向靜態數據的指針。這可通過兩種方法避免:
- 返回從堆中動態分配的數據(即內存空間地址)。在這種情況下,調用者負責釋放堆中的存儲空間。其優點是不必修改函數的外部接口,但不能保證向后兼容。現有的單線程程序若不修改而直接使用修改后的函數,將不會釋放存儲空間,進而導致內存泄露。
- 由調用者提供存儲空間。盡管函數的外部接口需要改變,仍然推薦使用這種方法。
例如,將字符串轉換為大寫的strtoupper函數實現可能如下代碼片段所示:

1 /* non-reentrant function */ 2 char *strtoupper(char *string) 3 { 4 static char buffer[MAX_STRING_SIZE]; 5 int index; 6 7 for (index = 0; string[index]; index++) 8 buffer[index] = toupper(string[index]); 9 buffer[index] = 0; 10 11 return buffer; 12 }
該函數既不是可重入的,也不是線程安全的。使用第一種方法將其改寫為可重入的,函數將類似於如下代碼片段:

1 /* reentrant function (a poor solution) */ 2 char *strtoupper(char *string) 3 { 4 char *buffer; 5 int index; 6 7 /* error-checking should be performed! */ 8 buffer = malloc(MAX_STRING_SIZE); 9 10 for (index = 0; string[index]; index++) 11 buffer[index] = toupper(string[index]); 12 buffer[index] = 0; 13 14 return buffer; 15 }
更好的解決方案是修改接口。調用者須為輸入和輸出字符串提供存儲空間,如下代碼片段所示:

1 /* reentrant function (a better solution) */ 2 char *strtoupper_r(char *in_str, char *out_str) 3 { 4 int index; 5 6 for (index = 0; in_str[index]; index++) 7 out_str[index] = toupper(in_str[index]); 8 out_str[index] = 0; 9 10 return out_str; 11 }
通過使用第二種方法,不可重入的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變量)需由調用者來維護。該函數的可重入版本實現可能如下代碼片段所示:

1 /* reentrant function */ 2 char reentrant_lowercase_c(char *string, int *p_index) 3 { 4 char c = 0; 5 6 /* no initialization - the caller should have done it */ 7 8 /* searches a lowercase character */ 9 for (; c = string[*p_index]; (*p_index)++) { 10 if (islower(c)) { 11 (*p_index)++; 12 break; 13 } 14 } 15 return c; 16 }
函數的接口和用法均發生改變。調用者每次調用時必須提供該字符串,並在首次調用前將索引(index)初始化為0,如下代碼片段所示:

1 char *my_string; 2 char my_char; 3 int my_index; 4 ... 5 my_index = 0; 6 while (my_char = reentrant_lowercase_c(my_string, &my_index)) { 7 ... 8 }
編寫線程安全函數
在多線程程序中,所有被多線程調用的函數都必須是線程安全的。然而,在多線程程序中可變通地使用線程不安全的子例程。注意,不可重入的函數通常都是線程不安全的,但將其改寫為可重入時,一般也會使其線程安全。
對共享資源加鎖
使用靜態數據或其它任何共享資源(如文件或終端)的函數,必須對這些資源加“鎖”來串行訪問,以使該函數線程安全。例如,以下函數是線程不安全的:

1 /* thread-unsafe function */ 2 int increment_counter() 3 { 4 static int counter = 0; 5 6 counter++; 7 return counter; 8 }
為使該函數線程安全,靜態變量counter需要被靜態鎖保護,如下例(偽代碼)所示:

1 /* pseudo-code thread-safe function */ 2 int increment_counter(); 3 { 4 static int counter = 0; 5 static lock_type counter_lock = LOCK_INITIALIZER; 6 7 lock(counter_lock); 8 counter++; 9 unlock(counter_lock); 10 return counter; 11 }
在使用線程庫的多線程應用程序中,應使用信號量互斥鎖(mutex)來串行訪問共享資源,獨立庫可能需要工作於線程上下文之外,因此使用其他類型的鎖。
線程不安全函數的變通方案
多線程變通地調用線程不安全函數是可能的。這在多線程程序使用線程不安全庫時尤其有用,如用於測試或待該庫的線程安全版本可用時再予以替換。該變通方案會帶來一些開銷,因為需對整個函數甚至一組函數進行串行化。
- 對該庫使用全局鎖,每次使用庫(調用庫內子例程或使用庫內全局變量)時均對其加鎖,如下偽代碼片段所示:

1 /* this is pseudo-code! */ 2 lock(library_lock); 3 library_call(); 4 unlock(library_lock); 5 6 lock(library_lock); 7 x = library_var; 8 unlock(library_lock);
該方案可能產生性能瓶頸,因為任一時刻僅有一個線程可訪問庫的任一部分。僅當不常訪問庫,或作為初步快速實現的權宜之計時可以采用該方案。
- 對每個庫組件(例程或全局變量)或一組組件使用鎖,如下例偽代碼片段所示:

1 /* this is pseudo-code! */ 2 lock(library_moduleA_lock); 3 library_moduleA_call(); 4 unlock(library_moduleA_lock); 5 6 lock(library_moduleB_lock); 7 x = library_moduleB_var; 8 unlock(library_moduleB_lock);
該方案實現相比前者稍微復雜,但可提高性能。
該方案應僅用於應用程序而非庫,故可用互斥鎖對庫加鎖。
可重入和線程安全庫
可重入和線程安全庫廣泛應用於並行(和異步)編程環境,而不僅僅用於線程內。因此,總是使用和編寫可重入和線程安全的函數是良好的編程實踐。
使用函數庫
AIX基本操作系統附帶的幾個代碼庫是線程安全的。在AIX當前版本中,以下庫是線程安全的。
- C標准函數庫(libc.a)
- BSD兼容函數庫(libbsd.a)
某些標准C子例程是不可重入的,如ctime和strtok子例程。它們的可重入版本函數名是原始子例程名添加"_r"后綴。
在編寫多線程程序時,應使用子例程的可重入版本來替代原有版本。例如,以下代碼片段:

1 token[0] = strtok(string, separators); 2 i = 0; 3 do { 4 i++; 5 token[i] = strtok(NULL, separators); 6 } while (token[i] != NULL);
在多線程程序中應替換為以下代碼片段:

1 char *pointer; 2 ... 3 token[0] = strtok_r(string, separators, &pointer); 4 i = 0; 5 do { 6 i++; 7 token[i] = strtok_r(NULL, separators, &pointer); 8 } while (token[i] != NULL);
線程不安全庫可用於單線程程序中。程序員必須確保使用該庫的線程唯一性;否則,程序行為不可預料,甚至可能崩潰。
改寫函數庫
以下信息突出了將現有庫轉換為可重入和線程安全庫的主要步驟(僅適用於C語言代碼庫)。
- 識別對外的全局變量。這些變量通常在頭文件中用export關鍵字定義*。
【譯者注:應為”用extern關鍵字聲明”】
應封裝對外的全局變量。該變量應改為私有(在庫源代碼內用static關鍵字定義)。應創建(讀寫)該變量的子例程。
- 識別靜態變量和其他共享資源。靜態變量通常用static關鍵字定義。
任一共享資源均應與鎖關聯。鎖的粒度及數目會影響庫的性能。可使用”一次性初始化”特性(如pthread_once)來方便地初始化鎖。
- 識別不可重入函數並使之變為可重入函數。見”編寫可重入函數“。
- 識別線程不安全函數並使之變為線程安全函數。見”編寫線程安全函數“。