thread-safe 函數給多個 thread 同時重複呼叫時不會造成錯誤,包括原本就已經是 reentrant 的函數,以及用 mutex 或其它方式改良過的函式。
thread 排程不是 preemptive 會嗎?
以 threadFunc() 為例,對全域變數 glob 作 loops 次「加一」。當有兩個 thread t1 和 t2 都呼叫此函數,「加一」動作並不是 atomic operation,實際上包括了「讀 glob」、「glob 加 1」、和「寫 glob」,有時中間會插入另一個 thread,而造成結果被覆蓋而錯誤。t1 讀 glob 為 1
t2 讀 glob 為 1
t2 加 1 後寫 glob 為 2
t1 加 1 後寫 glob 為 2
結果是 2,但正確結果是 3。
註:glob 是 int,所以 read 跟 write 可以一次完成。如果資料量大於 int 或沒對齊 int boundary,read 跟 write 無法一次完成,也可能插入其它 thread 的動作。
改正方式:
- 使用 mutex (如有 mutex 的 threadFunc())。例如一個變數配置一個 mutex,用在存取需要 critical sections 的部份。
- 一次只讓一個 thread 執行 (serialized)。如果 thread 花很多時間執行此函數,會失去 concurrency。
reentrant 函數能夠不用 mutex 達到 thread safety。reentrant 函數
- 不使用全域變數 (要回給 caller 或呼叫間需要保留的任何資訊,必須存在 caller 配置的 buffers。)
- 不改變自身程式碼
- 不呼叫 no-reentrant 函數
有些標準函數不是 reentrant 的,但有另外提供名稱後置 _r 的 reentrant 版本,這些函數需要 buffer 回傳結果。
例如 asctime_r()、ctime_r()、getgrgid_r()、getgrnam_r()、getlogin_r()、getpwnam_r()、getpwuid_r()、gmtime_r()、localtime_r()、rand_r()、readdir_r()、strerror_r()、strtok_r()、及 ttyname_r()。
glibc 提供的 crypt_r()、gethostbyname_r()、getservbyname_r()、getutent_r()、getutid_r()、getutline_r()、及 ptsname_r()。
參考:
- 書:《The Linux Programming Interface》chap. 31.1
- 可重入與執行緒安全 (reentrant vs thread-safe) Part 1/Part 2/Part 3
- coroutine
- https://en.wikipedia.org/wiki/Non-blocking_algorithm
- https://en.wikipedia.org/wiki/Real-time
- Emulated atomic operations and real-time scheduling,處理器原生只支援最基本的 atomic operations,如何 emulate 其它 atomic operation?在Linux kernel 只需要暫時關閉中斷 (SMP 不行)。
必須避免執行緒同時修改資料結構,破壞了資料的正確性
如果要鎖住資料結構,通常需要 read-modify-write
- 檢查目前是否有人正在修改 (有 read 動作)
- 如果沒人在修改,就先「鎖上」後修改 (modify-write)
POSIX 提供高階的鎖定機制,如:semaphore、mutex、spinlock、rwlock