重入和線程安全
在整個文檔中,"重入"和 "線程安全 "這兩個術語被用來標記類和函數,以表明它們在多線程應用程序中的使用方式:
- 線程安全函數可以同時被多個線程調用,即使調用使用的是共享數據,因為共享數據的所有引用都已序列化。
- 可重入函數也可以同時被多個線程調用,但前提是每次調用都使用自己的數據。
因此,線程安全的函數總是可重入的,但可重入的函數并不總是線程安全的。
推而廣之,如果一個類的成員函數可以被多個線程安全調用,只要每個線程使用的是該類的不同實例,那么這個類就是可重入的。如果可以從多個線程安全地調用類的成員函數,即使所有線程都使用類的相同實例,該類也是線程安全的。
注意:?只有當 Qt 類被多個線程使用時,才會被記錄為線程安全。如果函數未標記為線程安全或可重入,則不應在不同線程中使用。如果一個類未標記為線程安全或可重入,則該類的特定實例不得從不同線程訪問。
重入
C++ 類通常是可重入的,原因很簡單,因為它們只訪問自己的成員數據。任何線程都可以調用可重入類實例的成員函數,只要其他線程不能同時調用該類同一實例的成員函數。例如,下面的Counter
?類就是可重入類:
class Counter
{
public:Counter() { n = 0; }void increment() { ++n; }void decrement() { --n; }int value() const { return n; }private:int n;
};
該類不是線程安全的,因為如果多個線程試圖修改數據成員n
?,結果是未定義的。這是因為++
?和--
?操作符并不總是原子性的。事實上,它們通常擴展為三條機器指令:
- 將變量值載入寄存器。
- 遞增或遞減寄存器的值。
- 將寄存器的值存儲回主內存。
如果線程 A 和線程 B 同時加載變量的舊值、遞增寄存器并將其存儲回去,那么它們最終會互相覆蓋,而變量只會遞增一次!
導致類不可重入的典型設計模式
- 使用靜態數據成員或全局狀態
- 如果類依賴靜態變量或全局資源,多個實例或線程共享這些狀態時可能引發沖突。
- 單例模式未正確實現線程安全
- 單例類若在初始化時未加鎖,多線程可能創建多個實例,破壞單例語義。
- 未保護共享外部資源
- 類若操作文件、數據庫連接等外部資源時未加鎖,多線程訪問會導致資源沖突。
- 依賴非線程安全的第三方庫
- 若類封裝了非線程安全的第三方 API,直接暴露給多線程環境會導致不可重入。
- 成員函數修改共享內部狀態
- 若類的成員函數修改共享的成員變量,且未同步,多線程調用會破壞狀態。
- 比如緩存類(不可沖入):多線程調用?
add()
?可能導致?std::map
?內部狀態損壞。 -
class Cache { private:std::map<std::string, std::string> data_; public:void add(const std::string& key, const std::string& value) {data_[key] = value; // 多線程寫入可能破壞 map 結構} };
線程安全
顯然,訪問必須序列化:線程 A 必須不間斷(原子地)執行步驟 1、2、3,然后線程 B 才能執行相同的步驟;反之亦然。讓類具有線程安全的簡單方法是使用QMutex?保護對數據成員的所有訪問:
class Counter
{
public:Counter() { n = 0; }void increment() { QMutexLocker locker(&mutex); ++n; }void decrement() { QMutexLocker locker(&mutex); --n; }int value() const { QMutexLocker locker(&mutex); return n; }private:mutable QMutex mutex;int n;
};
QMutexLocker?類會在構造函數中自動鎖定互斥體,并在函數結束調用析構函數時解除鎖定。鎖定互斥確保來自不同線程的訪問將被序列化。mutex
?數據成員使用mutable
?限定符聲明,因為我們需要在value()
?中鎖定和解鎖互斥體,而 是一個常量函數。
Qt 類注意事項
許多 Qt 類都是可重入的,但它們并不是線程安全的,因為如果讓它們成為線程安全的,就會產生重復鎖定和解鎖QMutex?的額外開銷。例如,QString?是可重入的,但不是線程安全的。您可以安全地同時從多個線程訪問QString?的不同實例,但無法安全地同時從多個線程訪問QString?的同一實例(除非您使用QMutex?保護自己的訪問)。
有些 Qt 類和函數是線程安全的。這些主要是與線程相關的類(如QMutex?)和基本函數(如QCoreApplication::postEvent() )。
注:?多線程領域的術語并不完全標準化。POSIX 使用的可重入和線程安全定義與其 C API 有些不同。在 Qt 中使用其他面向對象的 C++ 類庫時,請務必理解這些定義。
Reentrancy and Thread-Safety | Qt 6.8