2008年12月19日 星期五

Reentrant and Thread-Safe Code



前些日子,在撰寫一個專案時,程式一直當在某一段function,後來先使用mutex方式來解決,但一直未認真研究問題在那,近日終於想到應該是多線程中reentrant的影響會導致程式當掉,為了提高系統效能,一定得修改這部份的程式碼,先來研究一下Reentrant。
下面這一篇就講的很詳細了。【引用來自chinaunix
-------------------------------------------------------------------------------------
記得以前討論過一個關於reentrant函數與thread safe函數的帖子,很多人對於這兩種函數不是很了解,尤其是發現malloc等函數是non-reentrant函數時,對多線程編程都產生了"恐懼",這裡是我對這兩種函數的一些理解,希望和大家探討一些.歡迎批評指正.

1. reentrant函數

一個函數是reentrant的,如果它可以被安全地遞歸或並行調用。要想成為reentrant式的函數,該函數不能含有(或使用)靜態(或全局)數據(來存儲函數調用過程中的狀態訊息),也不能返回指向靜態數據的指標,它只能使用由調用者提供的數據,當然也不能調用non-reentrant函數

比較典型的non-reentrant函數有getpwnam, strtok, malloc等

reentrant和non-reentrant函數的例子
CODE:

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys>
#include <unistd.h>
#include <math.h>

int* getPower(int i)
{
static int result;
result = pow(2, i);
getchar();
return result;
}

void getPower_r(int i, int* result)
{
*result = pow(2, i);
}

void handler (int signal_number) /*處理SIGALRM信號*/

{
getPower(3);
}

int main ()
{
int *result;
struct sigaction sa;
memset( sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigaction(SIGALRM, sa, NULL);
result = getPower(5);
printf("2^5 = %d\n", *result);
return 0;
}


試驗方法:
1. 編譯 gcc test.c -lpthread
在一個終端中營運 ./a.out, 在另一個終端中營運 ps -Agrep a.out可以看到該進程的id
2. 用如下模式營運a.out:
營運./a.out,在按返回前,在另外一個終端中營運kill -14 pid (這裡的pid是營運上面的ps時看到的值)
然後,按返回繼續營運a.out就會看到2^5 = 8 的錯誤結論


對於函數int* getPower(int i)

由於函數getPower會返回一個指向靜態數據的指標,在第一次調用getPower的過程中,再次調用getPower,則兩次返回的指標都指向同一塊內存,第二次的結果將第一次的覆蓋了(很多non-reentrant函數的這種用法會導致不確定的後果).所以是non-reentrant的.


對於函數void getPower_r(int i, int* result)

getPower_r會將所得的訊息存儲到result所指的內存中,它只是使用了由調用者提供的數據,所以是reentrant.在信號處理函數中可以正常的使用它.


2. thread-safe函數

Thread safety是多線程編程中的概念,thread safe函數是指那些能夠被多個線程同時並發地正確執行的函數.

thread safe和non thread safe的例子

CODE:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;

int count; /*共享數據*/

void* func (void* unused)
{
if (count == 0)
count++;
}

void* func_s (void* unused)
{
pthread_mutex_lock( sharedMutex); /*進入臨界區*/
if (count == 0)
count++;
pthread_mutex_unlock( sharedMutex); /*離開臨界區*/
}


int main ()
{
pthread_t pid1, pid2;
pthread_create( pid1, NULL, func, NULL);
pthread_create( pid2, NULL, func, NULL);
pthread_join(pid1, NULL);
pthread_join(pid2, NULL);
return 0;
}

函數func是non thread safe的,這是因為它不能避免對共享數據count的race condition,設想這種情況:一開始count是0,當線程1進入func函數,判斷過count == 0後,線程2進入func函數線程2判斷count==0,並執行count++,然後線程1開始執行,此時count != 0 了,但是線程1仍然要執行count++,這就產生了錯誤.

func_s透過mutex鎖將對共享數據的訪問鎖定,從而避免了上述情況的發生。func_s是thread safe的,只要透過適當的"鎖"機製,thread safe函數還是比較好實現的.

3. reentrant函數與thread safe函數的區別

reentrant函數與是不是多線程無關,如果是reentrant函數,那麼要求即使是同一個進程(或線程)同時多次進入該函數時,該函數仍能夠正確的運作.該要求還蘊含著,如果是在多線程環境中,不同的兩個線程同時進入該函數時,該函數也能夠正確的運作.

thread safe函數是與多線程有關的,它只是要求不同的兩個線程同時對該函數的調用在邏輯上是正確的.

從上面的說明可以看出,reentrant的要求比thread safe的要求更加嚴格.reentrant的函數必是thread safe的,而thread safe的函數
未必是reentrant的. 舉例說明:

CODE:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

pthread_mutex_t sharedMutex=PTHREAD_MUTEX_INITIALIZER;

int count; /*共享數據*/

void* func_s (void* unused)
{
pthread_mutex_lock( sharedMutex); /*進入臨界區*/
printf("locked by thead %d\n", pthread_self());
if (count == 0)
count++;
getchar();
pthread_mutex_unlock( sharedMutex); /*離開臨界區*/
printf("lock released by thead %d\n", pthread_self());
}

void handler (int signal_number) /*處理SIGALRM信號*/
{
printf("handler running in %d\n", pthread_self());
func_s(NULL);
}


int main ()
{
pthread_t pid1, pid2;
struct sigaction sa;
memset( sa, 0, sizeof(sa));
sa.sa_handler = handler;
sigaction(SIGALRM, sa, NULL);
printf("main thread's pid is: %d\n", pthread_self());
func_s(NULL);
pthread_create( pid1, NULL, func_s, NULL);
pthread_create( pid2, NULL, func_s, NULL);
pthread_join(pid1, NULL);
pthread_join(pid2, NULL);
func_s(NULL);
return 0;
}

試驗方法:
1. 編譯 gcc test.c -lpthread
在一個終端中營運 ./a.out, 在另一個終端中營運 ps -Agrep a.out可以看到該進程的id
2. 進行下面4次營運a.out:
每次營運分別在第1,2,3,4次返回前,在另外一個終端中營運kill -14 pid (這裡的pid是上面ps中看到的值)

試驗結果:
1. 該進程中有3個線程:一個主線程,兩個子線程
2. func_s是thread safe的
3. func_s不是reentrant的
4. 信號處理程式會中斷主線程的執行,不會中斷子線程的執行
5. 在第1,4次返回前,在另外一個終端中營運kill -14 pid會形成死鎖,這是因為
主線程先鎖住了臨界區,主線程被中斷後,執行handler(以主線程執行),handler試圖鎖定臨界區時,
由於同一個線程鎖定兩次,所以形成死鎖
6. 在第2,3次返回前,在另外一個終端中營運kill -14 pid不會形成死鎖,這是因為一個子線程先鎖住
了臨界區,主線程被中斷後,執行handler(以主線程執行),handler試圖鎖定臨界區時,被掛起,這時,子線程
可以被繼續執行.當該子線程釋放掉鎖以後,handler和另外一個子線程可以競爭進入臨界區,然後繼續執行.
所以不會形成死鎖.

結論:
1. reentrant是對函數相當嚴格的要求,絕大部分函數都不是reentrant的(APUE上有一個reentrant函數
的清單).
什麼時候我們需要reentrant函數呢?只有一個函數需要在同一個線程中需要進入兩次以上,我們才需要
reentrant函數.這些情況主要是異步信號處理,遞歸函數等等.(non-reentrant的遞歸函數也不一定會
出錯,出不出錯取決於你怎么定義和使用該函數). 大部分時候,我們並不需要函數是reentrant的.

2. 在多線程環境當中,只要求多個線程可以同時調用一個函數時,該函數只要是thread safe的就可以了.
我們常見的大部分函數都是thread safe的,不確定的話請查閱相關文檔.

3. reentrant和thread safe的本質的區別就在於,reentrant函數要求即使在同一個線程中任意地進入兩次以上,也能正確執行.

大家常用的malloc函數是一個典型的non-reentrant但是是thread safe函數,這就說明,我們可以方便的
在多個線程中同時調用malloc,但是,如果將malloc函數放入信號處理函數中去,這是一件很危險的事情.

4. reentrant函數肯定是thread safe函數,也就是說,non thread safe肯定是non-reentrant函數
不能簡單的透過加鎖,來使得non-reentrant函數變成 reentrant函數,這個鏈接是說明一些non-reentrant ===> reentrant和non thread safe ===>thread safe轉換的


最後有一些結論要記住:

* 識別出由函數庫匯出的所有全局變量。這些全局變量通常是在頭檔案中由export關鍵字定義的。
匯出的全局變量應該被封裝起來。每個變量應該被設為函數庫所私有的(透過static關鍵字實現),然後創建全局變量的訪問函數來執行對全局變量的訪問。
* 識別出所有靜態變量和其他共享資源。靜態變量通常是由static關鍵字定義的。
每個共享資源都應該與一個鎖關聯起來,鎖的粒度(也就是鎖的數量),影響著函數庫的性能。為了初始化所有鎖,可能需要一個僅被調用一次的初始化函數。
* 識別所有非可重入函數,並將其轉化為可重入。
* 識別所有非線程安全函數,並將其轉化為線程安全。

------------------------------------------------------------------------------------


後來發現我的subfunction中有個變數被宣告為static,無怪一直找不出bug在那,困擾許久的問題終於解決。



參考資料
http://publib.boulder.ibm.com/infocenter/systems/index.jsp?topic=/com.ibm.aix.genprogc/doc/genprogc/writing_reentrant_thread_safe_code.htm
http://blog.csdn.net/lovekatherine/archive/2007/03/28/1544585.aspx
http://www.ttisql.com/relatedlink/thread-safe.html
http://bbs.chinaunix.net/thread-971102-1-1.html

沒有留言: