執行緒安全與可重入(thread safety and reentrancy)
不是每個函式都能安全地讓好幾條執行緒同時呼叫。執行緒安全的函式,即使多條執行緒同時呼叫也行為正確;非執行緒安全的函式,若兩條執行緒同時跑它,就可能出錯——弄壞資料、回傳錯誤結果、或崩潰。想像一台公用影印機:若它的設計讓兩個人同時按「影印」各自都拿到自己正確的副本,它就「可安全共用」;若兩位同時使用者會卡紙或把彼此的頁面混在一起,它就不是。
一個函式為何可能不安全,通常歸結於藏在它裡面的共享可變狀態。經典的肇事者會跨呼叫保留靜態內部狀態。老舊的 strtok() 函式把字串切成詞元,它在呼叫之間用單一個內部靜態變數記住自己的位置;若兩條執行緒同時對不同字串做切分,它們會互相覆蓋那個共享位置,兩者都拿到垃圾。一個函式若沒有這種共享狀態、或用同步保護了它的共享狀態,就是執行緒安全的。一個密切相關的概念是可重入(reentrancy):若一個函式能在執行到一半時被安全地暫停、並在第一次呼叫完成前再次被呼叫(由另一條執行緒、或由信號處理常式、或由它自己),它就是可重入的。可重入函式避免靜態與全域狀態,只在傳入的或呼叫區域的資料上運作;正是這個性質讓它們在已在執行時能被安全地再次進入。
這在真實程式碼中為何重要:許多較舊的 C 函式庫函式因歷史因素是非可重入的,並有較安全的可重入表親,慣例上以 _r 後綴命名——strtok_r() 收一個由呼叫者明確提供的指標,而非藏起狀態,所以每條執行緒保有自己的、彼此不相撞。誠實的準則是:別只因為一個函式「看起來簡單」就假設它執行緒安全——查文件,因為危險在呼叫處是看不見的。而靠在一個函式外面包一把全域鎖來讓它執行緒安全雖然正確,卻可能把呼叫者串行化;把它設計成可重入(無狀態、在傳入資料上運作)則同時避開了錯誤與瓶頸。
strtok() 把它的掃描位置存在一個隱藏的靜態變數裡,所以兩條執行緒同時呼叫它會互相弄壞。strtok_r() 改成收一個由呼叫者擁有的指標參數,所以每條執行緒傳自己的、永不互相干擾——這就是可重入版本。
隱藏的靜態狀態讓函式不安全;可重入版本把狀態保存在呼叫者提供的儲存中。
執行緒安全與可重入有重疊但不相同:一個函式可用內部鎖做到執行緒安全,卻仍不可重入(從它自己的信號處理常式裡呼叫它,可能在那把鎖上死結)。拿不準時,優先選 _r 變體並讀手冊——「看起來簡單」不是保證。