1. 程式人生 > >reentrant函式與thread safe函式淺析

reentrant函式與thread safe函式淺析

記得以前討論過一個關於reentrant函式與thread safe函式的帖子
很多人對於這兩種函式不是很瞭解,
尤其是發現malloc等函式是non-reentrant函式時,對多執行緒程式設計都產生了"恐懼"
這裡是我對這兩種函式的一些理解,希望和大家探討一些.歡迎批評指正. 1. reentrant函式 一個函式是reentrant的,如果它可以被安全地遞迴或並行呼叫。要想成為reentrant式的函式,該函式不能含有(或使用)靜態(或全域性)資料(來儲存函式呼叫過程中的狀態資訊),也不能返回指向靜態資料的指標,它只能使用由呼叫者提供的資料,當然也不能呼叫non- reentrant函式. 比較典型的non-reentrant函式有getpwnam, strtok, malloc等. reentrant和non-reentrant函式的例子 #include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#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 -A|grep 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的例子 #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的. 舉例說明: #include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <string.h>
#include <sys/types.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 -A|grep 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轉換的
http://www.unet.univie.ac.at/aix/aixprggd/genprogc/writing_reentrant_thread_safe_code.htm
[ 本帖最後由 ypxing 於 2007-8-4 01:06 編輯 ] --------------------------------------------------------------------------------
lenovo 回覆於:2007-08-02 21:38:57 不錯,很好的帖子。
--------------------------------------------------------------------------------
科技牛 回覆於:2007-08-03 15:38:14 受教很深!
--------------------------------------------------------------------------------
ypxing 回覆於:2007-08-03 15:58:22 呼叫了malloc的函式肯定是non-reentrant的 引用:原帖由 bluster 於 2007-8-3 15:55 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155171&ptid=971102] 最後一點是錯的,比如一個函式呼叫malloc並不影響這個函式是否是reentrant。 --------------------------------------------------------------------------------
ypxing 回覆於:2007-08-03 15:59:35 這傢伙,怎麼把自己的帖子給刪了?
--------------------------------------------------------------------------------
bluster 回覆於:2007-08-03 16:01:11 引用:原帖由 ypxing 於 2007-8-3 15:58 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155198&ptid=971102]
呼叫了malloc的函式肯定是non-reentrant的 你是對的,我一時有點繞。
其實,是對reentrant的定義有問題。
可重入的意思,差不多是函式的任意部分都可以並行,而執行緒安全的意思則是多執行緒環境下使用沒有問題,對於非可重入的函式,使用lock來保護不可並行的部分從而執行緒安全。
引用:原帖由 ypxing 於 2007-8-3 15:59 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7155214&ptid=971102]
這傢伙,怎麼把自己的帖子給刪了? 無價值糊塗帖,所以刪了。 [ 本帖最後由 bluster 於 2007-8-3 16:05 編輯 ]
--------------------------------------------------------------------------------
jigloo 回覆於:2007-08-03 16:11:55 >>3. reentrant和thread safe的本質的區別就在於,reentrant函式要求在同一個執行緒中需要進入兩次以上,
並能正確執行.
--------------------------------------------------------------------------------
思一克 回覆於:2007-08-03 17:03:49 這個問題很複雜。 LZ的帖子很好。改進的地方是LZ應該多講WHY不可重入,如何才可重入,而不是下結論。 1)呼叫了不可重入函式的函式不一定是不可重入的。比如LINUX KERNEL中,裝置中斷處理函式是不可重入的,而__do_IRQ()呼叫了他們,但__do_IRQ卻是可重入的。
只要保證被呼叫的函式部分沒有重入就可以了。 2)使用的全域性變數的函式也不一定是不可重入的。還比如__do_IRQ()使用了全域性變數來儲存資料,但它是可重入的。 類似的例子:
[CODE]
int ia[32]; int func(int i)
{
    ia++;
    printf("%p i %d %d\n", &i, i, ia);
    if(i == 31) return;
    func(i+1);
} main()
{
    func(0); }
[/CODE] 關於這個問題,看LINUX中斷處理部分非常有啟發。那裡邏輯複雜,各種重入(硬,軟中斷,多CPU)處理的非常巧妙。
--------------------------------------------------------------------------------
ypxing 回覆於:2007-08-03 18:50:12 思一克,你好
首先謝謝你的鼓勵. 你給出的這個例子,函式func,既不是可重入的,也不是執行緒安全的,
原因如下: 假設有一個訊號處理函式handler,裡面呼叫了func
考慮這種情況:
主函式中呼叫了func(0) (這個時候,你的本意是先要ia[0]++,然後列印現在ia[0]的值,
再然後繼續後面的操作),
在func剛執行完ia[0]++時,訊號觸發了handler函式,
handler函式會呼叫func函式,然後執行對ia的一系列操作,完成後返回.
這時,你的主函式呼叫的func繼續執行,也就是要printf了,
這時printf的東東就不是你想要的了,而且你無法確定現在ia[0]的值是什麼(因為訊號
可以中斷很多次很多層).所以func不是可重入的. 而且也不是執行緒安全的. 可重入的一個判定方法就是將它放入訊號處理函式中,仔細推敲各種中斷情況下,
你是不是還能得到你想要的結果. "使用的全域性變數的函式也不一定是不可重入的。"這句是正確的,只要正確使用就可以了,
但是不使用全域性變數是寫可重入函式的簡單方法. "呼叫了不可重入函式的函式不一定是不可重入的。"這句是不對的,
因為你無法保證被呼叫的不可重入函式部分不被重入
int ia[32]; int func(int i)
{
    ia++;
    printf("%p i %d %d\n", &i, i, ia);
    if(i == 31) return;
    func(i+1);
} main()
{
    func(0); }
--------------------------------------------------------------------------------
思一克 回覆於:2007-08-03 19:39:57 你寫可重入函式時候要考慮到保證不可重入部分不重入, 還有保證整個函式必須可重入.
__do_IRQ就是如此.
所以說"呼叫了不可重入函式的函式不一定是不可重入的"是正確的.
而"呼叫了不可重入函式的函式一定是不可重入的"是不對的.因為有十分多的反例.
呼叫了不可重入函式的函式不一定是不可重入的。"這句是不對的,
因為你無法保證被呼叫的不可重入函式部分不被重入
--------------------------------------------------------------------------------
feasword 回覆於:2007-08-03 20:09:35 一直想找這兩個概念是此非彼的例子,受教了
關於死鎖的問題,apue裡也有講,以前也遇到過,當時乾脆都弄成遞迴鎖了
--------------------------------------------------------------------------------
ypxing 回覆於:2007-08-03 20:49:04 那麼,怎麼才能保證不可重入的部分不被重入呢? 引用:原帖由 思一克 於 2007-8-3 19:39 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156157&ptid=971102]
你寫可重入函式時候要考慮到保證不可重入部分不重入, 還有保證整個函式必須可重入.
__do_IRQ就是如此.
所以說"呼叫了不可重入函式的函式不一定是不可重入的"是正確的.
而"呼叫了不可重入函式的函式一定是不可 ... --------------------------------------------------------------------------------
cugb_cat 回覆於:2007-08-03 22:12:05 引用:原帖由 ypxing 於 2007-8-3 20:49 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156295&ptid=971102]
那麼,怎麼才能保證不可重入的部分不被重入呢? 我有同樓主相同的疑問。 另外,從lz的例子中學到一些技巧,關於除錯多執行緒程式,感謝lz。 [ 本帖最後由 cugb_cat 於 2007-8-3 22:45 編輯 ]
--------------------------------------------------------------------------------
飛灰橙 回覆於:2007-08-03 22:18:09 引用:原帖由 思一克 於 2007-8-3 19:39 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156157&ptid=971102]
你寫可重入函式時候要考慮到保證不可重入部分不重入, 還有保證整個函式必須可重入.
__do_IRQ就是如此.
所以說"呼叫了不可重入函式的函式不一定是不可重入的"是正確的.
而"呼叫了不可重入函式的函式一定是不可重入的"是不對的(語句A).因為有十分多的反例.
呼叫了不可重入函式的函式不一定是不可重入的。"這句是不對的(語句B),
因為你無法保證被呼叫的不可重入函式部分不被重入
越看越糊塗了,撇開討論的問題不談, 上面的語句A和語句B,必定有一句是錯的
--------------------------------------------------------------------------------
cugb_cat 回覆於:2007-08-03 22:44:57 引用:原帖由 飛灰橙 於 2007-8-3 22:18 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156605&ptid=971102]
越看越糊塗了,撇開討論的問題不談, 上面的語句A和語句B,必定有一句是錯的 兩句意思相反~:mrgreen:
--------------------------------------------------------------------------------
ypxing 回覆於:2007-08-03 23:30:14 俺也看了好一會才看懂:em02: 引用:原帖由 飛灰橙 於 2007-8-3 22:18 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7156605&ptid=971102]
越看越糊塗了,撇開討論的問題不談, 上面的語句A和語句B,必定有一句是錯的 --------------------------------------------------------------------------------
mingyanguo 回覆於:2007-08-04 00:08:35 完了,簡單的問題複雜化了 :mrgreen:
--------------------------------------------------------------------------------
hakase 回覆於:2007-08-08 20:37:06 好帖,受教了~~
--------------------------------------------------------------------------------
ypxing 回覆於:2007-08-08 23:05:51 這兩天寫了一個測試程式來驗證malloc的不可重入性
但是malloc一直沒有crash,有點鬱悶 過段時間把自己的測試程式碼貼出來,讓大家來幫忙看看
--------------------------------------------------------------------------------
bluster 回覆於:2007-08-09 10:08:56 引用:原帖由 ypxing 於 2007-8-8 23:05 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7176529&ptid=971102]
這兩天寫了一個測試程式來驗證malloc的不可重入性
但是malloc一直沒有crash,有點鬱悶 過段時間把自己的測試程式碼貼出來,讓大家來幫忙看看 多執行緒條件下,signal的handler有可能在一個單獨的執行緒中執行,如果這樣那麼malloc用鎖保護就夠了。
--------------------------------------------------------------------------------
ypxing 回覆於:2007-08-09 10:29:51 在多執行緒條件下,
理論上,將malloc放入signal的handler也是會出問題的,
鎖是不行的,會死鎖 引用:原帖由 bluster 於 2007-8-9 10:08 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7177603&ptid=971102] 多執行緒條件下,signal的handler有可能在一個單獨的執行緒中執行,如果這樣那麼malloc用鎖保護就夠了。 --------------------------------------------------------------------------------
ypxing 回覆於:2007-08-09 16:22:20 試圖測試malloc不可重入性的程式碼如下:
main.c
/*這是主程式,用來呼叫malloc*/ #include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h> void setUnblock()
{
sigset_t sigset;
sigemptyset(&sigset);
sigprocmask(SIG_SETMASK, &sigset, NULL);
     
}
void usr1Handler (int signal_number)        /*處理SIGUSR1訊號*/
{
setUnblock(); /*使得SIGUSR1可以被巢狀*/
free((int*)malloc(sizeof(int)*1000));
//printf("enter handler\n");
//getchar();
} int main ()
{
int *pi;
struct sigaction sa;

memset(&sa, 0, sizeof(sa));
sa.sa_handler = &usr1Handler;
sigaction(SIGUSR1, &sa, NULL); pause();

return 0;
}
kill.c /*這個是用來發送SIGUSR1訊號的*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h> int main(int argc,char *argv[])
{
int i;
char killstr[30]="kill -USR1 ";
if (argc == 2)
{
    strcat(killstr, argv[1]);
}
   for (i=0; i<3; i++)
{
   fork();        /*這樣會有8個程序同時傳送*/
}

while(1)
{
    system(killstr);
}

return 0;
}
驗證方法是:
1. 編譯main.c 和kill.c
gcc main.c -o main
gcc kill.c -o kill 2. 執行./main
並在另外一個終端執行ps -A|grep main查找出該程序的程序號為pid 3. 執行./kill pid (此處pid為第二步查到的pid) 運行了很長時間,也沒有crash
請大家看看我的程式,討論一個測試方案出來 引用:原帖由 ypxing 於 2007-8-8 23:05 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7176529&ptid=971102]
這兩天寫了一個測試程式來驗證malloc的不可重入性
但是malloc一直沒有crash,有點鬱悶 過段時間把自己的測試程式碼貼出來,讓大家來幫忙看看 --------------------------------------------------------------------------------
mingyanguo 回覆於:2007-08-09 17:36:23 引用:原帖由 ypxing 於 2007-8-9 16:22 發表 [url=http://bbs.chinaunix.net/redirect.php?goto=findpost&pid=7180404&ptid=971102]
試圖測試malloc不可重入性的程式碼如下:
main.c
/*這是主程式,用來呼叫malloc*/ #include
#include
#include
#include
#include
#include void setUnblock()
{
sigset_t sigset;
s ... 我估計是因為現在的malloc是執行緒安全的原因所以不會crash但是死鎖。
我在debian上面的一個測試程式碼,會死鎖,top一下會發現程序狀態總是sleep #include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h> #if 0
#define PRINT(a) do { \
printf a;   \
fflush(stdout);   \
}while(0)
#else
#define PRINT(a)
#endif static void
run_malloc(void)
{
void *mem[8];
int sz;
int i; for (i = 0; i < (sizeof(mem)/sizeof(mem[0])); i++) {
   sz = random() % (1024 * 1024);
   if (sz <= 0)
    sz = 1024;
   mem = malloc(sz);
   if (mem == NULL) {
    PRINT (("[%d] malloc null...\n", i));
    exit(-1);
   }
   PRINT(("%d\n", i));
   snprintf(mem, sz, "this is a test...");
} for (--i; i >= 0; i--) {
   free(mem);
}
} static void
sighandler(int signo)
{
static void *mem = NULL; PRINT ((".\n"));
if (mem == NULL) {
   mem = malloc(1024);
} else {
   free(mem);
   mem = NULL;
}
} static void
malloc_loop(void)
{ for (;;)
   run_malloc();
} static void
signal_loop(pid_t child)
{
int usec; for (;;) {
   kill(child, SIGUSR1);
   usec = ((unsigned int)random()) % 10;
   usleep(usec);
}
} int
main(int argc, char **argv)
{
pid_t child; if ((child = fork()) < 0) {
   perror("fork()");
   exit(-1);
} else if (child == 0) {
   /* child */
   if (signal(SIGUSR1, sighandler) < 0) {
    perror("signal");
    exit(-1);
   }
   malloc_loop();
} else {
   /* parent */
   signal_loop(child);
} return 0;
}
--------------------------------------------------------------------------------
haohao06 回覆於:2007-08-10 11:45:06 謝謝樓主講解.收藏先
--------------------------------------------------------------------------------
system888net 回覆於:2008-02-23 12:12:14 頂...
--------------------------------------------------------------------------------
dxcnjupt 回覆於:2008-02-23 19:46:50 不知道這個理解對不對:
thread-safe和reentrant的區別:在發生中斷時,高優先順序程式碼搶佔,此時若低優先順序程式碼持有鎖,則高優先順序程式碼會一直等待鎖開啟,但是低優先順序程式碼失去了排程機會,於是造成死鎖。thread-safe不考慮這種情況,但是reentrant需要。 實現reentrant的幾種方法:
1不使用臨界區,把原先的全域性/靜態變數變成函式引數,由函式呼叫者維護。優點是實現簡單,缺點是函式功能的封裝性可能會受到影響。
2在進入臨界區之前,關中斷(遮蔽訊號)。優點是實現簡單,缺點是影響實時效能,在多核機器上可能引起瓶頸(幾個核等待一個核釋放訊號量)。
3嘗試加鎖,無法加鎖返回一個出錯值,而不是一直等待下去。缺點是出錯處理比較麻煩
4為一組臨界量開啟一個專門的執行緒進行處理。優點是可以對臨界區的訪問按優先順序排序,以及其它可擴充套件操作,缺點是效能受到IPC的影響。
5使用lock-free結構取代鎖。缺點是lock-free演算法很多都需要memory-copy,影響效率。