【譯】編寫可重入和線程安全的代碼


 

編寫可重入和線程安全的代碼

(http://www.ualberta.ca/dept/chemeng/AIX-43/share/man/info/C/a_doc_lib/aixprggd/genprogc/writing_reentrant_thread_safe_code.htm)

 

     單線程的進程中僅有一個控制流。這種進程執行的代碼無需可重入或線程安全。在多線程的程序中,同一函數或資源可能被多個控制流並發訪問。為保護資源完整性,多線程程序編碼必須可重入且線程安全。

     本節提供了一些編寫可重入和線程安全程序的(指導)信息,但不包括編寫線程高效程序的主題。線程高效程序是高效並行化的程序,僅可在程序設計中實現。現有的單線程程序可變得線程高效,但這需要完全地重新設計和重寫。

Ÿ 

理解可重入性和線程安全性

     可重入和線程安全與函數處理資源的方式有關。可重入和線程安全是兩個相互獨立的概念:一個函數可以僅是可重入的,可以僅是線程安全的,可以兩者皆是或兩者皆不是。

可重入性

     可重入函數不能為后續的調用保持靜態(或全局)數據,也不能返回指向靜態(或全局)數據的指針。函數中用到的所有數據,都應由函數調用者提供(不包括棧上的局部數據)。可重入函數不能調用不可重入的函數。

     不可重入的函數經常(但不總是)可以通過其外部接口和用法識別。例如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 }
View Code

     使用全局數據是線程不安全的。應為每個線程維護一份全局數據的拷貝或封裝全局數據,以使對它的訪問變成串行的。線程可能讀取另一線程造成的錯誤對應的錯誤碼。在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 }
View Code

     該函數既不是可重入的,也不是線程安全的。使用第一種方法將其改寫為可重入的,函數將類似於如下代碼片段:

 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 }
View Code

     更好的解決方案是修改接口。調用者須為輸入和輸出字符串提供存儲空間,如下代碼片段所示:

 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 }
View Code

     通過使用第二種方法,不可重入的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;
}
View Code

     該函數是不可重入的。為使它可重入,靜態數據(即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 }
View Code

     函數的接口和用法均發生改變。調用者每次調用時必須提供該字符串,並在首次調用前將索引(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 }
View Code

 

編寫線程安全函數

     在多線程程序中,所有被多線程調用的函數都必須是線程安全的。然而,在多線程程序中可變通地使用線程不安全的子例程。注意,不可重入的函數通常都是線程不安全的,但將其改寫為可重入時,一般也會使其線程安全。

對共享資源加鎖

     使用靜態數據或其它任何共享資源(如文件或終端)的函數,必須對這些資源加“鎖”來串行訪問,以使該函數線程安全。例如,以下函數是線程不安全的:

1 /* thread-unsafe function */
2 int increment_counter()
3 {
4         static int counter = 0;
5  
6         counter++;
7         return counter;
8 }
View Code

     為使該函數線程安全,靜態變量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 }
View Code

     在使用線程庫的多線程應用程序中,應使用信號量互斥鎖(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);
View Code

     該方案可能產生性能瓶頸,因為任一時刻僅有一個線程可訪問庫的任一部分。僅當不常訪問庫,或作為初步快速實現的權宜之計時可以采用該方案。

  • 對每個庫組件(例程或全局變量)或一組組件使用鎖,如下例偽代碼片段所示: 
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);
View Code

     該方案實現相比前者稍微復雜,但可提高性能。

     該方案應僅用於應用程序而非庫,故可用互斥鎖對庫加鎖。

 

可重入和線程安全庫

     可重入和線程安全庫廣泛應用於並行(和異步)編程環境,而不僅僅用於線程內。因此,總是使用和編寫可重入和線程安全的函數是良好的編程實踐。

使用函數庫

     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);
View Code

     在多線程程序中應替換為以下代碼片段:

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);
View Code

     線程不安全庫可用於單線程程序中。程序員必須確保使用該庫的線程唯一性;否則,程序行為不可預料,甚至可能崩潰。

改寫函數庫

    以下信息突出了將現有庫轉換為可重入和線程安全庫的主要步驟(僅適用於C語言代碼庫)。

  • 識別對外的全局變量。這些變量通常在頭文件中用export關鍵字定義*

       【譯者注:應為”用extern關鍵字聲明”】

       應封裝對外的全局變量。該變量應改為私有(在庫源代碼內用static關鍵字定義)。應創建(讀寫)該變量的子例程。

  • 識別靜態變量和其他共享資源。靜態變量通常用static關鍵字定義。

        任一共享資源均應與鎖關聯。鎖的粒度及數目會影響庫的性能。可使用”一次性初始化”特性(如pthread_once)來方便地初始化鎖。

  • 識別不可重入函數並使之變為可重入函數。見”編寫可重入函數“。
  • 識別線程不安全函數並使之變為線程安全函數。見”編寫線程安全函數“。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM