什么是線程安全?
一個函數被多個並發線程反復調用時,它會一直產生正確的結果,則該函數是線程安全函數。
那么什么又是可重入函數?
當一個函數在被一個線程調用時,可以允許被其他線程再調用。即兩個函數“同時”發生。則該函數是可重入函數。
所以,顯而易見,如果一個函數是可重入的,那么它肯定是線程安全的。但反之未然,一個函數是線程安全的,卻未必是可重入的。比如我們在一個函數中調用到了一個全局變量NUM用來標記某一東西的數量。學個操作系統的同學都知道,如果我們在修改它的值的時候發生了中斷,兩一個函數又對他進行了修改,此時該變量的值會出錯。這種函數就是線程不安全函數。他是屬於沒有保護共享變量的線程不安全函數。在單線程時運行毫無問題,但一旦放到多線程中就容易出bug。但如果我們在修改這個全局變量NUM前對他進行加鎖,再操作完后再進行解鎖。這樣即使有兩個線程在調用這個函數,其結果也不會出問題。此時,這個函數就是線程安全函數。但他依舊不是可重入函數。因為他不能保證兩個函數“同時”運行,必須等待解鎖后才能運行。而我們在平時開發中應該盡量編寫可重入的函數。 如下圖:


線程不安全函數主要分為以下四大類:
第一類:不保護共享變量的函數,
a, 函數中訪問全局變量和堆。共享變量在多線程中是共享數據比如全局變量和堆,如果不保護共享變量,多線程時會出bug。
可以通過同步機制來保護共享數據,比如加鎖。
第二類:函數中分配,重新分配釋放全局資源。
與上面第一點基本相同,通過加鎖可解決
第三類:返回指向靜態變量的指針的函數,函數中通過句柄和指針的不直接訪問。
比如,我們要計算a,b兩個變量的和,於是將a,b的指針傳入某一個函數,然而此時可能有另一個線程改變了a,b的值,此時在函數中我們通過地址取到的兩個數的值已經改變了,所以計算出的結果也就是錯的了。
又比如某些函數(如gethostbyname)將計算結果放在靜態結構中,並返回一個指向這個結構的指針。在多線程中一個線程調用的結構可能被另一個線程覆蓋。可以通過重寫函數和加鎖拷貝技術來消除。加鎖拷貝技術指在每個位置對互斥鎖加鎖,調用線程不安全函數,動態的為結果分配存儲器,拷貝函數返回的結構,然后解鎖。
第四類:調用線程不安全函數
常見的系統線程不安全函數:
線程不安全函數 | 線程不安全 類 | unix線程安全版本 |
rand | 2 | rand_r |
strtok | 2 | strtok_r |
asctime | 3 | asctime_r |
ctime | 3 | ctime_r |
gethostbyaddr | 3 | gethostbyaddr_r |
geyhostbyname | 3 | gethostbyname_r |
inet_ntoa | 3 |
|
localtime | 3 | localtime_r |
UNIX環境高級編程列出 POSIX.1規范中的非線程安全的函數:
asctime | ecvt | gethostent | getutxline | putc_unlocked |
---|---|---|---|---|
basename | encrypt | getlogin | gmtime | putchar_unlocked |
catgets | endgrent | getnetbyaddr | hcreate | putenv |
crypt | endpwent | getnetbyname | hdestroy | pututxline |
ctime | endutxent | getopt | hsearch | rand |
dbm_clearerr | fcvt | getprotobyname | inet_ntoa | readdir |
dbm_close | ftw | getprotobynumber | L64a | setenv |
dbm_delete | getcvt | getprotobynumber | lgamma | setgrent |
dbm_error | getc_unlocked | getprotoent | lgammaf | setkey |
dbm_fetch | getchar_unlocked | getpwent | lgammal | setpwent |
dbm_firstkey | getdate | getpwnam | localeconv | setutxent |
dbm_nextkey | getenv | getpwuid | lrand48 | strerror |
dbm_open | getgrent | getservbyname | mrand48 | strtok |
dbm_store | getgrgid | getservbyport | nftw | ttyname |
dirname | getgrnam | getservent | nl_langinfo | unsetenv |
dlerror | gethostbyaddr | getutxent | ptsname | wcstombs |
drand48 | gethostbyname | getutxid | ptsname | ectomb |
目前大部分上述函數目前已經有了對應的線程安全版本的實現,例如:針對 getpwnam的 getpwnam_r(),( 這里的 _r表示可重入 (reentrant),如前所述,可重入的函數都是線程安全的)。在多線程軟件開發中,如果需要使用到上所述函數,應優先使用它們對應的線程安全版本。
因此在編寫線程安全函數時,要注意兩點:
- 1, 減少對臨界資源的依賴,盡量避免訪問全局變量,靜態變量或其它共享資源,如果必須要使用共享資源,所有使用到的地方必須要進行互斥鎖 (Mutex) 保護;
- 2, 線程安全的函數所調用到的函數也應該是線程安全的,如果所調用的函數不是線程安全的,那么這些函數也必須被互斥鎖 (Mutex) 保護;