Posted on 2008-12-29 17:11
lymons 閱讀(2700)
評論(0) 編輯 收藏 引用 所屬分類:
C++ 、
C 、
Unix/Linux 、
文章翻譯
UNIX上C++程序設計守則(6)
準則6: 遵守多線程編程的常識
- 要準確把握在POSIX標準的函數中,那些函數是非線程安全的,一定不要使用
- 要讓自己編寫的函數符合線程安全
- 在訪問共享數據/變量之前一定要先鎖定
- 如果使用C++的話,一定要注意函數的同步方法
說明: (1) 要準確把握那些非線程安全的函數,一定不要使用
如果在POSIX平臺上進行多線程編程時,有幾個最基本的知識,也就是所說的“常識”,希望大家一定要嚴格遵守。
...首先、我們要理解“線程安全”的意思。線程安全的函數就是指,“一個能被在多個線程同時調用也不會發生問題的函數”。這樣的函數通常要滿足以下幾個的特質。
- 不要操作局部的靜態變量(函數內的static變量)和全局靜態數據(全局變量,函數外的靜態變量)。而且,也不要調用其他的非線程安全的函數
- 如果要操作這樣的變量的話,事先必須使用互斥鎖mutex進行同步,否則一定要限制多個線程同時對它的訪問
那么、在POSIX標準的函數里面,也有不滿足上述條件的。由于歷史遺留問題,一些函數的識別標識(signature)的定義沒有考慮到線程安全的問題,所以不管怎么做都不能滿足上述的條件。例如,看看 localtime函數吧。它的定義的識別標識(signature)如下:
struct tm *localtime(const time_t *timer);
localtime 函數是,把一個用整數形式表示的時刻(從1970/1/1到現在為止的秒數)、轉換成一個能讓人容易明白的年月日形式表示出來的tm結構體并返回給調用者的函數。根據規格說明、返回出來的tm結構體是不需要free()掉,也不能釋放的。這個函數典型的實現就像下面的代碼那樣:
struct tm *localtime(const time_t *timer) {
static struct tm t;
/* ... 從timer參數里算出年月日等數值 ... */
t.tm_year = XXX;
/* ...把它們填入到結構體內... */
t.tm_hour = XXX;
t.tm_min = XXX;
t.tm_sec = XXX;
return &t;
}
這個函數如果被像下面那樣使用的話,就會有漏洞:
- 在線程A里執行 ta = localtime(x);
- 在線程B里執行 tb = localtime(y);
- 線程A參照ta結構體里的數據 → 就發現這些數據是一些奇怪的值!
...在函數的說明手冊里對這個問題也沒有做過詳細的說明。關于這個漏洞,在localtime函數即使使用了mutex鎖也不能被回避掉。所以,這個函數定義的識別標識是不行滴。
[譯者lymons注:在多個線程里調用localtime函數之所以有問題的原因是,localtime函數里返回的tm構造體是一個靜態的結構體,所以在線程A里調用localtime函數時,該結構體被賦予正確的值;而在線程A參照這個結構體之前,線程B又調用localtime的話,這個靜態的結構體又被賦予新的一個值。因此在線程A對這個結構體的訪問都是基于一個錯誤的值進行的]
正因為如此,就像上面說過的POSIX規格(SUSv3)里整齊的
asctime, basename, catgets, crypt, ctime, dbm_clearerr, dbm_close, dbm_delete, dbm_error, dbm_fetch, dbm_firstkey, dbm_nextkey, dbm_open, dbm_store, dirname, dlerror, drand48, ecvt, encrypt, endgrent, endpwent, endutxent, fcvt, ftw, gcvt, getc_unlocked, getchar_unlocked, getdate, getenv, getgrent, getgrgid, getgrnam,
(省略)
對于在規格中被定義為非線程安全的函數,應該制定一個避免使用它們的規則出來,并且制作一個能夠自動檢查出是否使用了這些函數的開發環境,應該是比較好的。
反之,在這里沒有被登載的POSIX標準函數都被假定為 "shall be thread-safe" 的、所以在實際的使用中可以認為在多線程環境里是沒有問題的(而且在使用的平臺上沒有特別地說明它是非線程安全的話)。
另外,有幾個非線程安全的函數,都準備了一個備用的線程安全版本的函數(僅僅是變更了函數的識別標識)。像這些函數為了與原版進行區別都在其函數名后面添加了 _r 這個后綴。例如,asctime函數就有線程安全版本的函數asctime_r。在規格說明中是否定義了備用函數,可以試著點擊剛才的那個網頁里面的函數名就可以看到。點擊 rand函數就可以看到,
[TSF] int rand_r(unsigned *seed);
用[TSF]這樣的文字標記出來的函數吧。這就是備用函數。在一覽中沒有記載出來的函數(備注: 稍微有點兒出入。請參照這里)、據我所知還有下面的備用函數。
asctime_r, ctime_r, getgrgid_r, getgrnam_r, getpwnam_r, getpwuid_r, gmtime_r, localtime_r, rand_r, readdir_r, strerror_r, strtok_r
還有,在規格以外,還準備了很多的下面那樣的函數。
gethostbyname_r, gethostbyname2_r
在最近的操作系統中、也使用 getaddrinfo API函數來解決IPv6名字對應的問題。gethostbyname系列的API都是比較陳舊的函數了、所以使用前面的函數還是比較好吧。根據規格SUSv3,getaddrinfo也是線程安全的:
The freeaddrinfo() and getaddrinfo() functions shall be thread-safe.
在多線程編程中,不要使用非線程安全的函數,而他們的備用函數可以放心地積極的去使用。
→后續