天天看點

翻譯 | 可重入與線程安全

❝Qt君今天在Qt幫助文檔中看到一篇不錯的文章,翻譯分享給大家。❞
翻譯 | 可重入與線程安全

  在整個文檔中,術語:「可重入和線程安全」用于标記類和函數,以表示它們如何在多線程應用程式中使用:

  • 「即使在調用使用共享資料時,也可以從多個線程同時調用線程安全的函數,因為對共享資料的所有引用都是序列化的」。
  • 「也可以從多個線程同時調用可重入函數,但前提是每次調用都使用自己的資料」。

「是以,線程安全的函數總是可重入的,但可重入的函數并不總是線程安全的」。

  引申開來,如果一個類的成員函數可以從多個線程安全地調用,則稱該類是可重入的,隻要每個線程使用該類的不同執行個體。如果可以從多個線程安全地調用該類的成員函數,即使所有線程使用該類的同一執行個體,該類也是線程安全的。

「注意」:Qt類隻有在被多個線程使用時才會被記錄為線程安全的。如果函數未标記為線程安全或可重入,則不應從不同的線程使用它。如果一個類沒有标記為線程安全或可重入,則不應該從不同的線程來通路該類的特定執行個體。

可重入

  C++類通常是可重入的,因為它們隻通路自己的成員資料。任何線程都可以在可重入類的執行個體上調用成員函數,隻要沒有其他線程可以同時在該類的同一執行個體上調用成員函數。例如,下面的Counter類是可重入的:

class Counter
{
public:
    Counter() { n = 0; }

    void increment() { ++n; }
    void decrement() { --n; }
    int value() const { return n; }

private:
    int n;
};
           

複制

  但是,這個類不是線程安全的,因為如果多個線程試圖修改資料成員

n

,結果是未定義的。這是因為

++n

--n

運算符并不總是原子性的。實際上,它們通常會擴充到這三個機器指令:

  1. 在寄存器中加載變量的值。
  2. 寄存器值的遞增或遞減。
  3. 将寄存器的值存儲回主記憶體中。

  如果線程A和線程B同時加載變量的舊值,增加它們的寄存器,并将其存儲回去,它們最終會互相覆寫,造成的後果是變量

n

隻增加一次!

線程安全

  顯然,通路必須是序列化的:線程A必須執行上述步驟123中的原子性不中斷,然後線程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類在其構造函數中自動鎖定互斥鎖,并在調用析構函數時在函數結束時解鎖它。鎖定互斥鎖可以確定來自不同線程的通路将被序列化。互斥鎖資料成員使用可變限定符聲明的,因為我們需要在value()中鎖定和解鎖互斥鎖,同時它還是一個const修飾的函數。

關于Qt類的注釋

「許多Qt類是可重入的,但它們不是線程安全的,因為使它們成為線程安全會導緻重複鎖定和解鎖一個QMutex的額外開銷」。例如,QString是可重入的,但不是線程安全的。您可以同時從多個線程安全地通路不同的QString執行個體,但是不能同時從多個線程安全地通路相同的QString執行個體(除非您使用QMutex保護自己的通路)。

  一些Qt類和函數是線程安全的。這些類主要是與線程相關的類(如QMutex)和基本函數(如QCoreApplication::postEvent())。

「注意」:多線程領域的術語并不是完全标準化的。POSIX使用可重入和線程安全的定義,這與它的C語言API有些不同。在Qt中使用其他面向對象的C++類庫時,請確定了解這些定義。