定義
可重入(reentrant)的定義1:
在單個線程中先后執行一段代碼是安全的,所謂安全,即一段代碼執行的時候,其不會因為進程的signal打斷而產生不一致的結果(以及產生的副作用,如更改的全局變量)。signal中斷如下:
可重入(reentrant)的定義2:但是,如果參考POSIX的定義,a "reentrant function" is defined as a "function whose effect, when called by two or more threads, is guaranteed to be as if the threads each executed the function one after another in an undefined order, even if the actual execution is interleaved". 即多個線程可以同時調用函數而保證安全,那么可重入的意義其實就是線程安全了。
線程安全(thread-safe)的定義:
一個線程內運行的程序不會因為多個線程的運行而產生跟單個線程運行的時候產生不一致的結果或副作用。
舉例:純函數,即不會影響全局相關變量的函數;或者是獲取static變量,線程共享變量的時候有加鎖的線程;這些情況下的代碼是線程安全的。
0.可重入跟線程安全的關系
可重入程序中可以實現線程安全,但可重入不一定線程安全。反之,線程安全代碼也不一定是可重入的。
一般可重入程序的概念只在signal中斷的情況下有討論意義,且單線程(進程)程序中的中斷便可以用於討論可重入。
下面的示例分別展示了可重入/不可重入、線程安全/不安全的各種情況;
1.不可重入/非線程安全
int tmp; void swap(int* x, int* y) { tmp = *x; *x = *y; /* Hardware interrupt might invoke isr() here. */ *y = tmp; } void isr() { int x = 1, y = 2; swap(&x, &y); }
其中swap函數不是thread-safe的,因為全局變量tmp可能會因為多線程調用導致一個線程的執行中tmp發生變化,導致*y的值發生競態條件,其運行結果會有變化;
也不是reentrant 可重入的,在該代碼中的注釋位置,在中斷的時候可以調用isr,在新的isr中如果tmp被賦值,完成調用后回到原線程,則*y的值會不一致;
2.可重入/非線程安全(按照舊定義)
可重入的函數一般也是線程安全的,但是有很多反例,如下:
int tmp; int add10(int a) { tmp = a; return a + 10; }
該代碼仍然有可能在任意地方被signal打斷,但是由於在單線程中返回的值與全局共享的tmp無關,所以是可重入的。但是這個不是thread-safe的,因為tmp在該調用過程中可能被其他線程修改。
在這個例子中,對於add10內的變量來說,可重入意味着修改操作對函數執行結果無影響。
反例2:
int t;
void swap(int *x, int *y) { int s; s = t; t = *x; *x = *y; y = t; t = s; }
交換兩個數,其中會臨時修改全局變量,但是t最后會被恢復;該函數按照定義1是可重入的,定義2則不然,因為它並不線程安全。考慮單線程情況下,即使A被B打斷了,仍然可以重入;但是多個線程執行的時候A從t=*x之后,另一個進程B覆蓋掉t的值,並在切回線程A的時候覆蓋掉y的值,直接導致線程不安全。
3.不可重入/線程安全
thread_local int tmp; int add10(int a) { tmp = a; return tmp + 10; }
該函數是thread-safe的,因為tmp是thread_local變量,各個線程之間的修改不會影響函數結果;
但是它不是可重入的,因為tmp可能在tmp+10處因為signal中斷導致返回的結果發生變化(在同一個線程內)。
該例子是對全局沒有影響,但是對局部有了影響。
4.可重入/線程安全
int add10(int a) { return a + 10; }
該代碼是thread-safe的:因為它不涉及多線程內變量的干擾;
是可重入的:因為它的變量即使被打斷了,a的值也不變,返回的結果仍然是a+10。
實際場景
1.signal中斷的可重入
實際代碼中signal中斷如果是自己寫的,那么要避免產生關聯函數內互相干擾狀態的問題;如果是跟別人的代碼一起工作的,那也要注意避免寫到全局變量;
2. malloc
malloc函數因為是對全局內存進行分配的,所以是不可重入的;但是一般對malloc的實現默認是線程安全的。
3. 標准I/O庫的不可重入
標准I/O函數,標准I/O庫的很多實現都以不可重入的方式使用全局數據結構。例如,書上或網上一些例子,信號處理函數中調用了printf,僅僅是為了直觀說明程序的運行,實際生產代碼中printf不能在信號處理函數中調用。
4.如何保證線程安全性
避免訪問並修改全局變量,static變量,如果要訪問,使用mutex保證線程安全;
5. 有一些函數雖然不要求thread-safe,但是也有thread-safe的實現,一般只把它們當作not thread-safe的;
clib中的有些函數是not reentrant的,但是也有reentrant的版本,如rand_r, srand_r,如下:
總結
1.signal可重入和遞歸調用的區別:
signal可重入是在任意的一個位置因為信號而被切換出去的,在堆棧上沒有直接關聯;而遞歸調用是在確定的位置調用代碼的,堆棧有父子關系。
2.
可重入不一定線程安全,線程安全不一定可重入;
但是實際遇到的情況:
程序可重入->大概率線程安全;
線程安全->跟可重入沒有太大關系,因為可重入是只針對單個線程內的信號的,跟線程的干擾不一樣;而且實際編碼中signal內的處理函數是自己寫的,只要不寫出干擾了全局狀態的代碼,或者加了自身的非reentrant鎖,一般也不會出現問題。
References
[1]Reentrancy, Wikipedia https://en.wikipedia.org/wiki/Reentrancy_(computing)
[2]Is Malloc Thread-Safe? https://stackoverflow.com/questions/855763/is-malloc-thread-safe
[3]日常開發筆記總結(六) https://www.52coder.net/post/weeknote-6
[4]可重入與執行緒安全 (reentrant vs thread-safe) Part1,https://magicjackting.pixnet.net/blog/post/113860339
[5]C-Language Use and Implementation of Interfaces https://pubs.opengroup.org/onlinepubs/9699919799/functions/V2_chap02.html#tag_15_09_01