並發編程基本模型
message passing和shared memory。
線程同步的四項原則
- 盡量最低限度地共享對象,減少需要同步的場合。如果確實需要,優先考慮共享 immutable 對象。
- 使用高級的並發編程構件,如TaskQueue、Producer-Consumer Queue、CountDownLatch等等。
- 不得已必須使用底層同步原語(primitives)時,只用非遞歸的互斥器和條件變量,慎用讀寫鎖,不要用信號量。
- 除了使用 atomic 整數之外,不自己編寫 lock-free 代碼,也不要用“內核級”同步原語。不憑空猜測“哪種做法性能會更好”,比如 spin lock vs. mutex。
互斥器的使用
- 用 RAII 手法封裝 mutex 的創建、銷毀、加鎖、解鎖這四個操作。保證鎖的生效期間等於一個作用域(scope)。
- 只用非遞歸的 mutex(即不可重入的 mutex)。
- 不手工調用 lock() 和 unlock() 函數,一切交給棧上的 Guard 對象的構造和析構函數負責(Scoped Locking)。
- 在每次構造 Guard 對象的時候,思考一路上(調用棧上)已經持有的鎖,防止因加鎖順序不同而導致死鎖。
條件變量的使用
-
對於 wait() 端:
必須與 mutex 一起使用,該布爾表達式的讀寫需受此 mutex 的保護。
在 mutex 已上鎖的情況下才能調用 wait()。
把判斷布爾表達式和 wait() 放在 while 循環中。 -
對於 signal/broadcast 端:
不一定要在 mutex 已上鎖的情況下調用 signal(理論上)。
在 signal 之前一般要修改布爾表達式。
修改布爾表達式通常需要用 mutex 保護(至少用作 full memory barrier)。
broadcast 通常用於表明狀態變化,signal 通常用於表示資源可用。 -
虛假喚醒(spurious wakeup),Linux 中 futex 慢速系統調用被信號打斷返回 -1,wait 返回了。
讀寫鎖與信號量的使用
- 從正確性方面來說,一種典型的易犯的錯誤是在持有 reader lock 的時候修改了共享數據。
- 從性能方面來說,讀寫鎖不見得比普通 mutex 更高效。
- reader lock 可能允許提升為 writer lock,也可能不允許提升。
- 通常 reader lock 是可重入的,writer lock 是不可重入的。
- 信號量不是必備的同步原語,因為條件變量配合互斥器可以完全替代其功能,而且更不易用錯。
參考:《Linux多線程服務端編程》。
